├── .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 | 
13 | 
14 | 
15 | 
16 | 
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 | 
27 | 
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 | [](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 | Display the contents of your clipboard and allow you to use it as a search query with a single click?
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 | Underline case citations and show the case name on hover on supported websites?
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 |
25 | Do you have login credentials for any of the following institutions? Setting this option allows Clerkent to redirect you to the appropriate WestLaw / LexisNexis / LawNet login page
26 |
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 |
14 | The default keyboard shortcut for opening the search popup is
15 | Ctrl Space
16 | . To customise it,
17 | {
18 | Browser.isChrome() ?
19 | (
20 | <>
21 | visit
22 |
28 | chrome://extensions/shortcuts
29 |
30 | . Due to Chrome's quirks, you will need to delete and add this shortcut once in order for it to work.
31 | >
32 | ) : (
33 | <>
34 | visit
35 |
41 | about:addons
42 |
43 | and click "Manage Extension Shortcuts", as shown in
44 |
50 | this video
51 |
52 | >
53 | )
54 | }
55 |
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 |
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 |
46 | )
47 | }
48 |
49 | export default JurisdictionSelect
--------------------------------------------------------------------------------
/src/Popup/components/LegislationResult.tsx:
--------------------------------------------------------------------------------
1 | import type { FunctionComponent } from 'preact'
2 | import Constants from 'utils/Constants'
3 | import Helpers from 'utils/Helpers'
4 | import ResultLink from './ResultLink'
5 |
6 | interface Props {
7 | legislation: Law.Legislation,
8 | downloadPDF: downloadPDFType,
9 | }
10 |
11 | const LegislationResult: FunctionComponent = ({
12 | legislation,
13 | downloadPDF,
14 | }) => {
15 | const {
16 | database,
17 | jurisdiction,
18 | provisionNumber,
19 | provisionType,
20 | statute,
21 | links,
22 | } = legislation
23 | const link = links[0]
24 | const pdfLink = Helpers.getPDFLink(links)
25 |
26 | return (
27 |
65 | )
66 | }
67 |
68 | export default LegislationResult
--------------------------------------------------------------------------------
/src/Popup/components/PopupContainer.tsx:
--------------------------------------------------------------------------------
1 | import type { FunctionComponent } from "preact"
2 |
3 | const PopupContainer: FunctionComponent = ({ children }) => (
4 |
7 | )
8 |
9 | export default PopupContainer
--------------------------------------------------------------------------------
/src/Popup/components/QueryResult.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'preact/hooks'
2 | import CaseResult from './CaseResult'
3 | import ShowMore from './ShowMore'
4 | import AnimatedLoading from '../../components/AnimatedLoading'
5 | import type { FunctionComponent } from 'preact'
6 | import ShowNewResultsButton from './ShowNewResultsButton'
7 |
8 | interface Props {
9 | searchResults: Law.Case[],
10 | downloadPDF: downloadPDFType,
11 | isSearching: boolean,
12 | updatePending: boolean,
13 | updateResults: () => void
14 | }
15 |
16 | const maxResults = 3
17 |
18 | const QueryResult: FunctionComponent = ({
19 | searchResults,
20 | downloadPDF,
21 | isSearching,
22 | updatePending,
23 | updateResults,
24 | }) => {
25 | const [morePressed, setMorePressed] = useState(false)
26 | const onShowMore = useCallback(() => setMorePressed(true), [])
27 |
28 | if(!isSearching && searchResults.length === 0){
29 | return No cases found
30 | }
31 |
32 | const showMore = morePressed || searchResults.length <= maxResults
33 |
34 | return (
35 |
36 | {
37 | (searchResults as Law.Case[])
38 | .slice(0, showMore ? undefined : maxResults)
39 | .map((result) => (
40 |
45 | ))
46 | }
47 | { showMore ? null :
}
48 |
49 | {
50 | updatePending ? (
51 |
52 | ) : null
53 | }
54 |
55 | {
56 | isSearching ? (
57 |
58 | ) : null
59 | }
60 |
61 | )
62 | }
63 |
64 | export default QueryResult
--------------------------------------------------------------------------------
/src/Popup/components/ResultLink.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'preact/hooks'
2 | import type { FunctionComponent } from 'preact'
3 | import Helpers from 'utils/Helpers'
4 | import PDFSvg from 'assets/icons/pdf.svg'
5 | import DOCXSvg from 'assets/icons/docx.svg'
6 |
7 | interface Props {
8 | empty?: boolean,
9 | link: Law.Link,
10 | onDownloadPDF: () => void
11 | }
12 |
13 | const PDFLink = ({
14 | href,
15 | onClick,
16 | }) => {
17 | return (
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | const DOCXLink = ({
25 | href,
26 | onClick,
27 | }) => {
28 | return null
29 | // return (
30 | //
31 | //
32 | //
33 | // )
34 | }
35 |
36 | const ResultLink: FunctionComponent = ({
37 | empty = false,
38 | link,
39 | onDownloadPDF,
40 | }) => {
41 | const onClick = useCallback((event) => {
42 | event.preventDefault()
43 | event.stopPropagation()
44 | onDownloadPDF()
45 | }, [onDownloadPDF])
46 | return link ? (
47 |
54 | {!empty && (
55 |
61 | {link.doctype}
62 |
63 | )}
64 |
65 | {/* {
66 | link.filetype === `PDF` ? (
67 |
68 | ) : (link.filetype === `DOCX` ? (
69 |
70 | ): null)
71 | } */}
72 |
73 | ) : null
74 | }
75 |
76 | export default ResultLink
--------------------------------------------------------------------------------
/src/Popup/components/ShowMore.tsx:
--------------------------------------------------------------------------------
1 | const ShowMore = ({ onClick }) => {
2 | return (
3 |
4 |
5 | Show More
6 |
7 |
8 | )
9 | }
10 |
11 | export default ShowMore
--------------------------------------------------------------------------------
/src/Popup/components/ShowNewResultsButton.tsx:
--------------------------------------------------------------------------------
1 | const ShowNewResultsButton = ({ onClick }) => {
2 | return (
3 |
6 | Show additional results
7 |
8 | )
9 | }
10 |
11 | export default ShowNewResultsButton
--------------------------------------------------------------------------------
/src/Popup/hooks/__tests__/__snapshots__/useSearch.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useSearch correctly sets search results 1`] = `
4 | [
5 | {
6 | "citation": "[2022] EWHC 160",
7 | "database": {
8 | "icon": "",
9 | "id": "UK_bailii",
10 | "name": "BAILII",
11 | "url": "https://www.bailii.org/",
12 | },
13 | "jurisdiction": "UK",
14 | "links": [],
15 | "name": "Stadler v Currys Group",
16 | "type": "case-citation",
17 | },
18 | {
19 | "citation": "[2022] EWHC 1379 (IPEC)",
20 | "database": {
21 | "icon": "",
22 | "id": "UK_bailii",
23 | "name": "BAILII",
24 | "url": "https://www.bailii.org/",
25 | },
26 | "jurisdiction": "UK",
27 | "links": [],
28 | "name": "Shazam Productions v Only Fools the Dining Experience",
29 | "type": "case-citation",
30 | },
31 | ]
32 | `;
33 |
--------------------------------------------------------------------------------
/src/Popup/hooks/__tests__/useSearch.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from "@testing-library/preact"
2 | import { Constants } from "utils"
3 | import useSearch from "../useSearch"
4 |
5 | const SEARCH_RESULTS = {
6 | done: true,
7 | results: [
8 | {
9 | citation: `[2022] EWHC 160`,
10 | database: Constants.DATABASES.UK_bailii,
11 | jurisdiction: Constants.JURISDICTIONS.UK.id,
12 | links: [],
13 | name: `Stadler v Currys Group`,
14 | type: `case-citation`,
15 | },
16 | {
17 | citation: `[2022] EWHC 1379 (IPEC)`,
18 | database: Constants.DATABASES.UK_bailii,
19 | jurisdiction: Constants.JURISDICTIONS.UK.id,
20 | links: [],
21 | name: `Shazam Productions v Only Fools the Dining Experience`,
22 | type: `case-citation`,
23 | },
24 | ] as Law.Case[],
25 | }
26 |
27 | describe(`useSearch`, () => {
28 | it(`has correct initial state`, () => {
29 | const { result } = renderHook(() => useSearch())
30 | const { current: { isSearching, searchResults } } = result
31 | expect(isSearching).toBe(false)
32 | expect(searchResults.length).toBe(0)
33 | })
34 | it(`correctly sets search results`, () => {
35 | const { result } = renderHook(() => useSearch())
36 | const { current: { onReceiveSearchResults } } = result
37 |
38 | act(() => {
39 | onReceiveSearchResults(SEARCH_RESULTS)
40 | })
41 |
42 | const { current: { searchResults } } = result
43 | expect(searchResults).toMatchSnapshot()
44 | })
45 | })
--------------------------------------------------------------------------------
/src/Popup/hooks/useClipboard.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback, useEffect } from 'preact/hooks'
2 | import { browser } from 'webextension-polyfill-ts'
3 |
4 | const useClipboard = () => {
5 | const [permissionGranted, setPermissionGranted] = useState(false)
6 |
7 | const promptGrant = useCallback(async () => {
8 | const permissionsRequest = await browser.permissions.request({
9 | permissions: [`clipboardRead`],
10 | })
11 | setPermissionGranted(permissionsRequest)
12 | }, [])
13 |
14 | useEffect(() => {
15 | (async () => {
16 | const granted = await browser.permissions.contains({ permissions: [`clipboardRead`] })
17 | setPermissionGranted(granted)
18 | })()
19 | })
20 |
21 | return {
22 | permissionGranted,
23 | promptGrant,
24 | }
25 | }
26 |
27 | export default useClipboard
--------------------------------------------------------------------------------
/src/Popup/hooks/useCustomiseDatabase.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from "preact/hooks"
2 | import { Constants, Storage } from "utils"
3 |
4 | const keys = {
5 | DATABASES_STATUS: `DATABASES_STATUS`,
6 | SELECTED_JURISDICTION: `POPUP_SELECTED_JURISDICTION`,
7 | }
8 |
9 | const useCustomiseDatabase = () => {
10 | const [selectedJurisdictionCode, setSelectedJurisdictionCode] = useState(Constants.JURISDICTIONS.UK.id)
11 | const [databasesStatus, setDatabasesStatus] = useState(Constants.DEFAULT_DATABASES_STATUS)
12 |
13 | const toggleDatabase = useCallback(async (id: string) => {
14 | const newStatus = {
15 | ...databasesStatus,
16 | [selectedJurisdictionCode]: {
17 | ...databasesStatus[selectedJurisdictionCode],
18 | [id]: !databasesStatus[selectedJurisdictionCode][id],
19 | },
20 | }
21 |
22 | await Storage.set(keys.DATABASES_STATUS, newStatus)
23 | setDatabasesStatus(newStatus)
24 | }, [selectedJurisdictionCode, databasesStatus])
25 |
26 | useEffect(() => {
27 | (async () => {
28 | const jurisdictionCode = await Storage.get(keys.SELECTED_JURISDICTION)
29 | setSelectedJurisdictionCode(jurisdictionCode)
30 |
31 | const databaseStatus: typeof Constants.DEFAULT_DATABASES_STATUS = await Storage.get(keys.DATABASES_STATUS) || Constants.DEFAULT_DATABASES_STATUS
32 | setDatabasesStatus(databaseStatus)
33 | })()
34 | }, [])
35 |
36 | const selectedJurisdiction = Constants.JURISDICTIONS[selectedJurisdictionCode]
37 |
38 | return {
39 | databasesStatus,
40 | selectedJurisdiction,
41 | toggleDatabase,
42 | }
43 | }
44 |
45 | export default useCustomiseDatabase
--------------------------------------------------------------------------------
/src/Popup/hooks/useFocusInput.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'preact/hooks'
2 |
3 | const useFocusInput = () => {
4 | const inputReference = useRef(null)
5 |
6 | useEffect(() => {
7 | if (inputReference.current) {
8 | inputReference.current.focus()
9 | }
10 | }, [inputReference.current])
11 |
12 | return inputReference
13 | }
14 |
15 | export default useFocusInput
--------------------------------------------------------------------------------
/src/Popup/hooks/useMessenger.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useCallback, useEffect } from 'preact/hooks'
2 | import type { Runtime } from 'webextension-polyfill-ts'
3 | import { browser } from 'webextension-polyfill-ts'
4 | import { Logger, Messenger } from 'utils'
5 |
6 | const useMessenger = ({ onReceiveSearchResults }) => {
7 | const port = useRef({} as Runtime.Port)
8 | const sendMessage = useCallback((message) => port.current.postMessage(message), [port])
9 | const onMessage = useCallback((message: Messenger.Message) => {
10 | Logger.log(`popup received:`, message)
11 | if(message.target !== Messenger.TARGETS.popup){
12 | return null // ignore
13 | }
14 | if(message.action === Messenger.ACTION_TYPES.search){
15 | const { data } = message
16 | onReceiveSearchResults(data)
17 | }
18 | }, [onReceiveSearchResults])
19 |
20 | const search = useCallback((citation, inputJurisdiction) => sendMessage({
21 | action: Messenger.ACTION_TYPES.search,
22 | citation,
23 | jurisdiction: inputJurisdiction,
24 | source: Messenger.TARGETS.popup,
25 | target: Messenger.TARGETS.background,
26 | }), [sendMessage])
27 |
28 | const downloadPDF = useCallback(
29 | ({ law, doctype }: { law: Law.Case | Law.Legislation, doctype: Law.Link[`doctype`]}) => () => sendMessage({
30 | action: Messenger.ACTION_TYPES.downloadPDF,
31 | doctype,
32 | law,
33 | source: Messenger.TARGETS.popup,
34 | target: Messenger.TARGETS.background,
35 | }), [sendMessage])
36 |
37 | useEffect(() => {
38 | port.current = browser.runtime.connect(``, { name: `popup-port` })
39 | sendMessage({ message: `popup says hi` })
40 | port.current.onMessage.addListener(onMessage)
41 | }, [port, onMessage, sendMessage])
42 |
43 | return {
44 | downloadPDF,
45 | onMessage,
46 | port,
47 | search,
48 | sendMessage,
49 | }
50 | }
51 |
52 | export default useMessenger
--------------------------------------------------------------------------------
/src/Popup/hooks/useSearch.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo, useRef, useState } from "preact/hooks"
2 |
3 | const useSearch = () => {
4 | const [isSearching, setIsSearchingRaw] = useState(false)
5 | const [newSearchResults, setNewSearchResults] = useState([] as Law.Case[])
6 | const [searchResults, setSearchResults] = useState([] as Law.Case[])
7 | const [initialTimerDone, setInitialTimerDone] = useState(false)
8 | const timer = useRef(null)
9 |
10 | const updateResults = useCallback(() => {
11 | if(newSearchResults.length > 0){
12 | setSearchResults(newSearchResults)
13 | setNewSearchResults([])
14 | }
15 | }, [newSearchResults])
16 |
17 | const setIsSearching = useCallback((value: boolean) => {
18 | setIsSearchingRaw(value)
19 |
20 | setInitialTimerDone(false)
21 | if(timer?.current){
22 | clearTimeout(timer.current)
23 | }
24 |
25 | if(value === true){
26 | timer.current = setTimeout(() => {
27 | setInitialTimerDone(true)
28 | updateResults()
29 | }, 1000)
30 | }
31 | }, [updateResults])
32 |
33 | const onReceiveSearchResults = useCallback((data: { done: boolean, results: Law.Case[] }) => {
34 | const { done, results } = data
35 |
36 | if(done){
37 | setIsSearchingRaw(false)
38 | setInitialTimerDone(true)
39 | }
40 |
41 | // avoid sudden shifts in the layout
42 | if(searchResults.length === 0 && !initialTimerDone){
43 | setSearchResults(results)
44 | setNewSearchResults([])
45 | } else {
46 | setNewSearchResults(results)
47 | }
48 | }, [searchResults.length, setIsSearchingRaw, initialTimerDone, setInitialTimerDone])
49 |
50 | const updatePending = useMemo(() => (newSearchResults.length > 0), [newSearchResults])
51 |
52 | const resetSearchResults = useCallback(() => {
53 | setSearchResults([])
54 | setNewSearchResults([])
55 | }, [])
56 |
57 | return {
58 | isSearching,
59 | onReceiveSearchResults,
60 | resetSearchResults,
61 | searchResults,
62 | setIsSearching,
63 | updatePending,
64 | updateResults,
65 | }
66 | }
67 |
68 | export default useSearch
--------------------------------------------------------------------------------
/src/Popup/index.tsx:
--------------------------------------------------------------------------------
1 | import { render } from 'preact'
2 | import Popup from './Popup'
3 |
4 | const container = document.querySelector(`#popup-root`)
5 | render( , container)
--------------------------------------------------------------------------------
/src/Popup/views/CustomiseDatabase.tsx:
--------------------------------------------------------------------------------
1 | import PopupContainer from "Popup/components/PopupContainer"
2 | import { Link } from "preact-router/match"
3 | import type { FunctionComponent } from 'preact'
4 | import DatabaseOption from 'Popup/components/DatabaseOption'
5 | import useCustomiseDatabase from 'Popup/hooks/useCustomiseDatabase'
6 | import { Constants } from 'utils'
7 |
8 | const getName = (jurisdictionCode: Law.JurisdictionCode, databaseId: string): string => {
9 | if(databaseId === `commonlii`){
10 | return Constants.DATABASES.commonlii.name
11 | }
12 | return Constants.DATABASES[`${jurisdictionCode}_${databaseId}`].name
13 | }
14 |
15 | const CustomiseDatabase: FunctionComponent = () => {
16 | const {
17 | selectedJurisdiction,
18 | databasesStatus,
19 | toggleDatabase,
20 | } = useCustomiseDatabase()
21 |
22 | return (
23 |
24 |
25 |
29 | Back
30 |
31 |
32 | {selectedJurisdiction.emoji} {selectedJurisdiction.name}
33 |
34 |
35 |
36 | Search the following databases:
37 |
38 |
39 | {Object.entries(databasesStatus[selectedJurisdiction.id]).map(([id, enabled]) => {
40 | const name = getName(selectedJurisdiction.id, id)
41 | return (
42 |
49 | )
50 | })}
51 |
52 |
53 | )
54 | }
55 |
56 | export default CustomiseDatabase
--------------------------------------------------------------------------------
/src/assets/icons/docx.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/pdf.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/Admonition.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'preact/hooks'
2 | import Helpers from '../utils/Helpers'
3 |
4 | const Admonition = ({
5 | className = ``,
6 | title = ``,
7 | children = null,
8 | onClick = () => null,
9 | }) => {
10 | const [shouldHide, setShouldHide] = useState(false)
11 | const dismissAdmonition = useCallback((event) => {
12 | event.stopPropagation()
13 | setShouldHide(true)
14 | }, [])
15 |
16 | return shouldHide ? null : (
17 |
25 |
26 |
27 | {title}
28 |
29 |
30 | {children}
31 |
32 |
33 |
✕
34 |
35 | )
36 | }
37 |
38 | export default Admonition
--------------------------------------------------------------------------------
/src/components/AnimatedLoading.tsx:
--------------------------------------------------------------------------------
1 | import type { FunctionComponent } from "preact"
2 |
3 | const AnimatedLoading: FunctionComponent = () => {
4 | return (
5 |
14 |
15 | )
16 | }
17 |
18 | export default AnimatedLoading
--------------------------------------------------------------------------------
/src/components/JurisdictionFlag.tsx:
--------------------------------------------------------------------------------
1 | import { getFlagSource } from 'utils/Flag'
2 |
3 | type Props = {
4 | id: Law.JurisdictionCode
5 | }
6 |
7 | const JurisdictionFlag = ({ id }: Props) => {
8 | const source = getFlagSource(id)
9 | const props = { className: `h-[20px]` }
10 | return (
11 |
12 | )
13 | }
14 |
15 | export default JurisdictionFlag
--------------------------------------------------------------------------------
/src/components/SelectInput.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'preact/hooks'
2 | import type { FunctionComponent, JSX } from 'preact'
3 | import { Helpers } from 'utils'
4 |
5 | interface Props {
6 | className?: string,
7 | options: {
8 | value: string,
9 | content: string,
10 | }[],
11 | value: string,
12 | onChange: (value: string) => void,
13 | defaultValue?: string,
14 | }
15 |
16 | const SelectInput: FunctionComponent = ({
17 | className = ``,
18 | options,
19 | value,
20 | onChange = (v) => {},
21 | defaultValue,
22 | }) => {
23 | const onSelect = useCallback(({ target }: Event) => {
24 | if(target instanceof HTMLSelectElement){
25 | onChange(target.value)
26 | }
27 | }, [onChange])
28 | const valueWithDefault = (((!value || !options.map(({ value }) => value).includes(value)) && defaultValue)
29 | ? defaultValue
30 | : value
31 | )
32 |
33 | return (
34 |
42 | {options.map(({ value, content }) => (
43 |
47 | {content}
48 |
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 |
24 |
25 |
26 | ,
27 | "container":
28 |
31 |
34 | Loading...
35 |
36 |
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 |
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 Ctrl Space .
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 |
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 | //
45 | //
DONE
46 | // {
47 | // parseResult.length > 0 ? (
48 | //
49 | //
50 | // The following cases were found. Click on any case to remove it from the list. When you're done, click the download button below.
51 | //
52 | //
53 | // {
54 | // parseResult.map(result => (
55 | //
56 | //
64 | // {Constants.JURISDICTIONS[result.jurisdiction].emoji}
65 | //
66 | //
67 | // {result.citation}
68 | //
69 | // ))
70 | // }
71 | //
72 | //
73 | // ) :
74 | // (pressedDone ? (
75 | //
No case citations found. Check to make sure the text does contain citations (note that Clerkent is as yet unable to recognise case names alone).
76 | // ) : null)
77 | // }
78 | //
79 | // )
80 | }
81 |
82 | export default MassCitations
--------------------------------------------------------------------------------
/src/pages/MassCitations/index.tsx:
--------------------------------------------------------------------------------
1 | import { render } from 'preact'
2 | import MassCitations from './MassCitations'
3 |
4 | const container = document.querySelector(`#mass-citations-root`)
5 | render( , container)
6 |
--------------------------------------------------------------------------------
/src/pages/Updates.tsx:
--------------------------------------------------------------------------------
1 | import type { FunctionComponent } from 'preact'
2 | import { render } from 'preact'
3 | import { useCallback } from 'preact/hooks'
4 | import 'styles/tailwind.css'
5 | import { Storage } from 'utils'
6 | import { browser } from 'webextension-polyfill-ts'
7 |
8 | const Updates: FunctionComponent = () => {
9 |
10 | const setDoNotRemind = useCallback(async () => {
11 | Storage.set(`DO_NOT_REMIND_SUBSCRIBE`, true)
12 | const tab = await browser.tabs.getCurrent()
13 | browser.tabs.remove(tab.id)
14 | }, [])
15 |
16 | return (
17 |
18 | Hey there
19 |
20 | I hope Clerkent has been useful for you.
21 | If you'd like to receive an email once in a
22 | few weeks about the latest changes and upcoming features,
23 | click here to subscribe to Clerkent Updates .
27 | Alternatively, you can subscribe to
28 | this RSS feed .
31 |
32 |
33 |
34 | Don't ask me to subscribe again
35 |
36 |
37 | )
38 | }
39 |
40 | const container = document.querySelector(`#clerkent-updates-root`)
41 | render( , container)
--------------------------------------------------------------------------------
/src/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer components {
6 | .pdf-filter {
7 | filter: invert(14%) sepia(89%) saturate(2676%) hue-rotate(350deg) brightness(84%) contrast(122%);
8 | }
9 |
10 | .loading-container > div {
11 | @apply absolute w-[10px] h-[10px] bg-slate-600 rounded-full;
12 | animation-timing-function: cubic-bezier(0, 1, 1, 0);
13 | }
14 | .loading-container > div:nth-child(1) {
15 | left: 4px;
16 | animation: lds-ellipsis1 0.6s infinite;
17 | }
18 | .loading-container > div:nth-child(2) {
19 | left: 4px;
20 | animation: lds-ellipsis2 0.6s infinite;
21 | }
22 | .loading-container > div:nth-child(3) {
23 | left: 16px;
24 | animation: lds-ellipsis2 0.6s infinite;
25 | }
26 | .loading-container > div:nth-child(4) {
27 | left: 28px;
28 | animation: lds-ellipsis3 0.6s infinite;
29 | }
30 |
31 | /* #clerkent-tooltip {
32 | @apply font-sans hidden fixed bg-white py-2 px-6 max-w-[15rem] z-[999] border border-solid border-black;
33 | }
34 |
35 | #clerkent-tooltip > .clerkent-meta {
36 | @apply flex flex-row content-center items-center mb-3;
37 | }
38 |
39 | #clerkent-tooltip > .clerkent-meta > span {
40 | @apply mr-2;
41 | }
42 |
43 | #clerkent-tooltip a {
44 | @apply text-black no-underline;
45 | }
46 |
47 | #clerkent-tooltip a[href] {
48 | @apply text-blue-600 cursor-pointer hover:underline;
49 | }
50 |
51 | #clerkent-tooltip > .clerkent-links a.clerkent-pdf > img {
52 | @apply h-4 ml-2 mt-[0.3rem] pdf-filter;
53 | }
54 |
55 | #clerkent-tooltip > .clerkent-links a.clerkent-pdf + a {
56 | @apply ml-4;
57 | }
58 |
59 | .clerkent.case {
60 | @apply underline decoration-dotted cursor-pointer;
61 | }
62 | .clerkent.case > .clerkent-tooltip {
63 | @apply hidden hover:block;
64 | } */
65 |
66 | kbd {
67 | @apply border border-solid border-gray-300 rounded bg-gray-100 p-0.5;
68 | }
69 | kbd + kbd {
70 | @apply ml-1;
71 | }
72 | }
73 |
74 | @keyframes lds-ellipsis1 {
75 | 0% {
76 | transform: scale(0);
77 | }
78 | 100% {
79 | transform: scale(1);
80 | }
81 | }
82 | @keyframes lds-ellipsis3 {
83 | 0% {
84 | transform: scale(1);
85 | }
86 | 100% {
87 | transform: scale(0);
88 | }
89 | }
90 | @keyframes lds-ellipsis2 {
91 | 0% {
92 | transform: translate(0, 0);
93 | }
94 | 100% {
95 | transform: translate(12px, 0);
96 | }
97 | }
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line quotes
2 | declare module '*.svg' {
3 | const content: any
4 | export default content
5 | }
6 |
7 | declare namespace Law {
8 | type Database = {
9 | icon: string,
10 | name: string,
11 | url: string,
12 | id: string,
13 | }
14 |
15 | type Link = {
16 | doctype: `Summary` | `Judgment` | `Opinion` | `Legislation` | `Order`,
17 | filetype: `PDF` | `HTML` | `DOCX` | `RTF`,
18 | url: string,
19 | }
20 | type Case = {
21 | name: string,
22 | citation: string,
23 | links: Link[]
24 | jurisdiction?: JurisdictionCode
25 | database: Database
26 | type?: Type
27 | }
28 | type Legislation = {
29 | provisionType: string,
30 | provisionNumber: string,
31 | statute: string,
32 | links: Link[],
33 | jurisdiction?: JurisdictionCode,
34 | content?: string,
35 | database: Database
36 | type?: Type
37 | }
38 |
39 | // ISO 3166-1 alpha-2
40 | // except for ECHR
41 | type JurisdictionCode = `SG` | `UK` | `EU` | `HK` | `CA` | `AU` | `NZ` | `MY` | `ECHR` | `UN` // `IN`
42 | type Jurisdiction = {
43 | emoji: string,
44 | id: JurisdictionCode,
45 | name: string
46 | }
47 |
48 | type SearchMode = `case` | `legislation`
49 | type Type = `case-citation` | `case-name` | `legislation`
50 | }
51 |
52 | interface RawCase extends Omit {
53 | citations: Law.Case[`citation`][],
54 | }
55 |
56 | type ValueOf = T[keyof T]
57 |
58 | declare namespace Messenger {
59 | type Message = {
60 | action: ValueOf
61 | source: ValueOf
62 | target: ValueOf
63 | data?: unknown
64 | }
65 |
66 | type OtherProperties = {
67 | [x: string]: any
68 | }
69 | }
70 |
71 | declare namespace Finder {
72 | type CaseNameFinderResult = {
73 | name: string,
74 | type: `case-name`
75 | }
76 |
77 | type FinderResult = CaseCitationFinderResult | LegislationFinderResult | CaseNameFinderResult
78 |
79 | type CaseCitationFinderResult = {
80 | jurisdiction: Law.JurisdictionCode
81 | citation: string,
82 | index: number,
83 | year?: string,
84 | court? : string,
85 | abbr?: string,
86 | page?: string,
87 | type: `case-citation`
88 | }
89 |
90 | type LegislationFinderResult = {
91 | provisionType: string,
92 | provisionNumber: string,
93 | statute: string,
94 | type: `legislation`,
95 | jurisdiction: Law.JurisdictionCode
96 | }
97 | }
98 |
99 | type InstitutionalLogin = `KCL` | `LSE` | `UCL` | `NTU` | `NUS` | `SMU` | `None`
100 |
101 | type ThenArgument = T extends PromiseLike ? U : T
102 |
103 | type downloadPDFType = (
104 | { law, doctype }: { law: Law.Case | Law.Legislation, doctype: Law.Link[`doctype`]}
105 | ) => () => void
--------------------------------------------------------------------------------
/src/utils/Browser.ts:
--------------------------------------------------------------------------------
1 | import { browser } from "webextension-polyfill-ts"
2 | import Memoize from 'memoizee'
3 |
4 | const getExtensionLink = Memoize(
5 | (string = ``) => browser.runtime.getURL(string),
6 | )
7 | const isFirefox = () => getExtensionLink().startsWith(`moz-extension://`)
8 | const isChrome = () => getExtensionLink().startsWith(`chrome-extension://`)
9 |
10 | const Browser = {
11 | getExtensionLink,
12 | isChrome,
13 | isFirefox,
14 | }
15 |
16 | export default Browser
17 |
--------------------------------------------------------------------------------
/src/utils/Clipboard.ts:
--------------------------------------------------------------------------------
1 | const readText = () => navigator.clipboard.readText()
2 |
3 | const isURI = (string: string) => (new RegExp(/.*(:\/\/|about:|chrome:).*/, `gi`)).test(string)
4 | const hasTooLongWord = (string: string) => {
5 | const matches = string.match(/\w+/g)
6 | if(matches === null){
7 | return false
8 | }
9 | return matches.sort(
10 | (a, b) => b.length - a.length,
11 | )[0].length > 30
12 | }
13 | const isTooLong = (string: string) => string.length > 300
14 | const hasInvalidCharacters = (string: string) => (new RegExp(/.*[<>\\{|}].*/, `gi`)).test(string)
15 |
16 | const getPopupSearchText = async (): Promise => {
17 | const clipboardText = await readText()
18 | const isValid = (
19 | clipboardText && clipboardText.length > 0 &&
20 | !isTooLong(clipboardText) && !isURI(clipboardText) &&
21 | !hasTooLongWord(clipboardText) && !hasInvalidCharacters(clipboardText)
22 | )
23 | if(isValid){
24 | return clipboardText
25 | }
26 | return ``
27 | }
28 |
29 | const Clipboard = {
30 | getPopupSearchText,
31 | readText,
32 | }
33 |
34 | export default Clipboard
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/CA.ts:
--------------------------------------------------------------------------------
1 | import Constants from '../../Constants'
2 | import { formatAbbrs, sortCasesByVolume } from './utils'
3 |
4 | export const CAAbbrs = [
5 | { abbr: `SCR` },
6 | { abbr: `SCC` },
7 | { abbr: `WWR` },
8 | { abbr: `EXP` },
9 | { abbr: `DLR`, appendum: `( \\(3d\\))?` },
10 | { abbr: `OR`, appendum: `( \\(3d\\))?` },
11 | { abbr: `CarswellAlta` },
12 | { abbr: `CarswellOnt` },
13 | { abbr: `OAC` },
14 | { abbr: `ABQB` },
15 | { abbr: `ABCA` },
16 | { abbr: `ABPC` },
17 | { abbr: `ABSC` },
18 | { abbr: `MBQB` },
19 | { abbr: `MBCA` },
20 | { abbr: `MBPC` },
21 | { abbr: `MBSC` },
22 | { abbr: `ONQB` },
23 | { abbr: `ONCA` },
24 | { abbr: `ONPC` },
25 | { abbr: `ONSC` },
26 | { abbr: `ONCJ` },
27 | { abbr: `BCQB` },
28 | { abbr: `BCPC`},
29 | { abbr: `BCCA` },
30 | { abbr: `BCSC` },
31 | { abbr: `LSBC` },
32 | { abbr: `SKQB` },
33 | { abbr: `SKCA` },
34 | { abbr: `SKPC` },
35 | { abbr: `SKSC` },
36 | { abbr: `NBQB` },
37 | { abbr: `NSQB` },
38 | { abbr: `NSCA` },
39 | { abbr: `NSPC` },
40 | { abbr: `NSSC` },
41 | { abbr: `BCQB` },
42 | { abbr: `BCPC` },
43 | { abbr: `BCCA` },
44 | { abbr: `BCSC` },
45 | { abbr: `QCQB`},
46 | { abbr: `QCCA` },
47 | { abbr: `QCCM` },
48 | { abbr: `QCCS` },
49 | { abbr: `QCCQ` },
50 | { abbr: `SCJ No` },
51 | { abbr: `CanLII` },
52 | ]
53 |
54 | export const sortCACases = (
55 | casesArray: Law.Case[],
56 | attribute: string,
57 | ): Law.Case[] => sortCasesByVolume(CAAbbrs, casesArray, attribute)
58 |
59 | export const findCACaseCitationMatches = (query: string) => {
60 | // eslint-disable-next-line unicorn/better-regex
61 | const yearRegex = new RegExp(/(([([])?[12]\d{3}(-[12]\d{3})?[)\]]?)/)
62 | const volumeRegex = new RegExp(/( \d{1,2})?/)
63 | const pageRegex = new RegExp(/\d{1,4}/)
64 | const abbrs = formatAbbrs(CAAbbrs)
65 | const regex = new RegExp(`${yearRegex.source}${volumeRegex.source} (${abbrs}) ${pageRegex.source}`, `gi`)
66 | return [...query.matchAll(regex)]
67 | }
68 |
69 | export const findCACaseCitation = (query: string): Finder.CaseCitationFinderResult[] => {
70 | const matches = findCACaseCitationMatches(query)
71 | if (matches.length > 0) {
72 | return matches.map((match) => ({
73 | citation: match[0],
74 | index: match.index,
75 | jurisdiction: Constants.JURISDICTIONS.CA.id,
76 | })).map(c => ({ ...c, type: `case-citation` }))
77 | }
78 | return []
79 | }
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/CaseCitationFinder.ts:
--------------------------------------------------------------------------------
1 | import { findUKCaseCitation, sortUKCases } from './UK'
2 | import { findSGCaseCitation , sortSGCases } from './SG'
3 | import { findNZCaseCitation, sortNZCases } from './NZ'
4 | import { findHKCaseCitation, sortHKCases } from './HK'
5 | import { findCACaseCitation, sortCACases } from './CA'
6 | import { findAUCaseCitation, sortAUCases } from './AU'
7 | import { findEUCaseCitation, sortEUCases } from './EU'
8 | import { findMYCaseCitation, sortMYCases } from './MY'
9 | import { findECHRCaseCitation, sortECHRCases } from './ECHR'
10 | import { findUNCaseCitation, sortUNCases } from './UN'
11 | import Helpers from 'utils/Helpers'
12 |
13 | const findCaseCitation = (query: string): Finder.CaseCitationFinderResult[] => {
14 | const cleanQuery = Helpers.cleanQuery(query)
15 | return [
16 | ...findSGCaseCitation(cleanQuery),
17 | ...findUKCaseCitation(cleanQuery),
18 | ...findEUCaseCitation(cleanQuery),
19 | ...findHKCaseCitation(cleanQuery),
20 | ...findCACaseCitation(cleanQuery),
21 | ...findAUCaseCitation(cleanQuery),
22 | ...findNZCaseCitation(cleanQuery),
23 | ...findMYCaseCitation(cleanQuery),
24 | ...findECHRCaseCitation(cleanQuery),
25 | ...findUNCaseCitation(cleanQuery),
26 | ]
27 | }
28 |
29 | const CaseCitationFinder = {
30 | findAUCaseCitation,
31 | findCACaseCitation,
32 | findCaseCitation,
33 | findECHRCaseCitation,
34 | findEUCaseCitation,
35 | findHKCaseCitation,
36 | findNZCaseCitation,
37 | findSGCaseCitation,
38 | findUKCaseCitation,
39 | findUNCaseCitation,
40 | sortAUCases,
41 | sortCACases,
42 | sortECHRCases,
43 | sortEUCases,
44 | sortHKCases,
45 | sortMYCases,
46 | sortNZCases,
47 | sortSGCases,
48 | sortUKCases,
49 | sortUNCases,
50 | }
51 |
52 | export default CaseCitationFinder
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/ECHR.ts:
--------------------------------------------------------------------------------
1 | import Constants from '../../Constants'
2 |
3 | const ECtHRApplicationRegex = new RegExp(/\b\d{5}\/\d{2}/)
4 | const ECtHROSCOLARegex = new RegExp(/[12[]\d{3}] ECHR \d{1,3}/)
5 | const ECtHRRegex = new RegExp(`${
6 | ECtHRApplicationRegex.source
7 | }|${
8 | ECtHROSCOLARegex.source
9 | }`, `gi`)
10 | // TODO: parse ECLI
11 |
12 | export const sortECHRCases = (
13 | citationsArray: Law.Case[],
14 | attribute: string,
15 | ): Law.Case[] => citationsArray
16 |
17 | export const findECHRCaseCitation = (
18 | query: string,
19 | ): Finder.CaseCitationFinderResult[] => {
20 | const matches = [...query.matchAll(ECtHRRegex)]
21 | return matches.map((match) => ({
22 | citation: match[0],
23 | index: match.index,
24 | jurisdiction: Constants.JURISDICTIONS.ECHR.id,
25 | })).map(c => ({ ...c, type: `case-citation` }))
26 | }
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/EU.ts:
--------------------------------------------------------------------------------
1 | import Constants from '../../Constants'
2 |
3 | export const epoRegex = new RegExp(/\b[GJT][ _]?\d{1,4}\/\d{1,2}/)
4 | export const cjeuRegex = new RegExp(/\b[CT]-\d{1,3}\/\d{1,2}/)
5 | // TODO: parse ECLI
6 |
7 | export const sortEUCases = (
8 | citationsArray: Law.Case[],
9 | attribute: string,
10 | ): Law.Case[] => citationsArray
11 |
12 | export const findEUCaseCitation = (
13 | query: string,
14 | ): Finder.CaseCitationFinderResult[] => {
15 | const regex = new RegExp(`(${epoRegex.source})|(${cjeuRegex.source})`, `gi`)
16 | const cleanedQuery = query
17 | .replaceAll(new RegExp(`[${String.fromCharCode(8209)}]`, `g`), `-`)
18 | .replaceAll(/case /gi, `C-`)
19 |
20 | const matches = [...cleanedQuery.matchAll(regex)]
21 |
22 | return matches.map((match) => ({
23 | citation: match[0],
24 | court: match[1] ? Constants.COURTS.EU_epo.id : Constants.COURTS.EU_cjeu.id,
25 | index: match.index,
26 | jurisdiction: Constants.JURISDICTIONS.EU.id,
27 | })).map(c => ({ ...c, type: `case-citation` }))
28 | }
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/HK.ts:
--------------------------------------------------------------------------------
1 | import Constants from '../../Constants'
2 | import { formatAbbrs, sortCasesByVolume } from './utils'
3 |
4 | export const HKAbbrs =[
5 | { abbr: `HKCFA` },
6 | { abbr: `HKCFAR` },
7 | { abbr: `HKCA` },
8 | { abbr: `HKC` },
9 | { abbr: `HKLRD` },
10 | { abbr: `HKCFI` },
11 | { abbr: `HKDC` },
12 | { abbr: `HKFC` },
13 | { abbr: `HKLdT` },
14 | { abbr: `HKCT` },
15 | { abbr: `HKCrC` },
16 | { abbr: `HKOAT` },
17 | { abbr: `HKLaT` },
18 | { abbr: `HKMagC` },
19 | ]
20 |
21 | export const sortHKCases = (
22 | citationsArray: Law.Case[],
23 | attribute: string,
24 | ): Law.Case[] => sortCasesByVolume(HKAbbrs, citationsArray, attribute)
25 |
26 | export const findHKCaseCitationMatches = (query: string) => {
27 | // eslint-disable-next-line unicorn/better-regex
28 | const yearRegex = new RegExp(/(([([])[12]\d{3}(-[12]\d{3})?[)\]])/)
29 | const volumeRegex = new RegExp(/( \d{1,2})?/)
30 | const pageRegex = new RegExp(/\d{1,4}/)
31 | const abbrs = formatAbbrs(HKAbbrs)
32 | const regex = new RegExp(`${yearRegex.source}${volumeRegex.source} (${abbrs}) ${pageRegex.source}`, `gi`)
33 | return [...query.matchAll(regex)]
34 | }
35 |
36 | export const findHKCaseCitation = (query: string): Finder.CaseCitationFinderResult[] => {
37 | const matches = findHKCaseCitationMatches(query)
38 | if (matches.length > 0) {
39 | return matches.map((match) => ({
40 | citation: match[0],
41 | index: match.index,
42 | jurisdiction: Constants.JURISDICTIONS.HK.id,
43 | })).map(c => ({ ...c, type: `case-citation` }))
44 | }
45 | return []
46 | }
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/MY.ts:
--------------------------------------------------------------------------------
1 | import Constants from '../../Constants'
2 | import { formatAbbrs, sortCasesByVolume } from './utils'
3 |
4 | export const MYAbbrs = [
5 | { abbr: `MYFC` },
6 | { abbr: `UKPC` },
7 | { abbr: `MYCA` },
8 | { abbr: `MYMHC` },
9 | { abbr: `MYSSHC` },
10 | { abbr: `MLR` },
11 | { abbr: `MLJ` },
12 | { abbr: `CLJ` },
13 | ]
14 |
15 | export const sortMYCases = (
16 | citationsArray: Law.Case[],
17 | attribute: string,
18 | ): Law.Case[] => sortCasesByVolume(
19 | MYAbbrs,
20 | citationsArray,
21 | attribute,
22 | )
23 |
24 | export const findMYCaseCitationMatches = (query: string) => {
25 | const regex = new RegExp(`\\[[12]\\d{3}]( \\d{1,2})? (${
26 | formatAbbrs(MYAbbrs)
27 | }) \\d{1,4}`, `gi`)
28 | return [...query.matchAll(regex)]
29 | }
30 |
31 | export const findMYCaseCitation = (query: string): Finder.CaseCitationFinderResult[] => {
32 | const matches = findMYCaseCitationMatches(query)
33 | return matches.map((match) => ({
34 | citation: match[0],
35 | index: match.index,
36 | jurisdiction: Constants.JURISDICTIONS.MY.id,
37 | })).map(c => ({ ...c, type: `case-citation` }))
38 | }
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/NZ.ts:
--------------------------------------------------------------------------------
1 | import Constants from '../../Constants'
2 | import { formatAbbrs, sortCasesByVolume } from './utils'
3 |
4 | export const NZAbbrs = [
5 | { abbr: `NZSC` },
6 | { abbr: `NZPC` },
7 | { abbr: `NZCA` },
8 | { abbr: `NZAR` },
9 | { abbr: `NZLR` },
10 | { abbr: `NZHC` },
11 | { abbr: `ACJ` },
12 | { abbr: `NZILR` },
13 | { abbr: `NZGazLawRp` },
14 | { abbr: `NZFC` },
15 | { abbr: `NZDC` },
16 | ]
17 |
18 | export const sortNZCases = (
19 | citationsArray: Law.Case[],
20 | attribute: string,
21 | ): Law.Case[] => sortCasesByVolume(
22 | NZAbbrs,
23 | citationsArray,
24 | attribute,
25 | )
26 |
27 | export const findNZCaseCitationMatches = (query: string) => {
28 | // eslint-disable-next-line unicorn/better-regex
29 | const yearRegex = new RegExp(/(([([])[12]\d{3}(-[12]\d{3})?[)\]])/)
30 | const volumeRegex = new RegExp(/( \d{1,2})?/)
31 | const pageRegex = new RegExp(/\d{1,4}/)
32 | const abbrs = formatAbbrs(NZAbbrs)
33 | const regex = new RegExp(`${yearRegex.source}${volumeRegex.source} (${abbrs}) ${pageRegex.source}`, `gi`)
34 |
35 | return [...query.matchAll(regex)]
36 | }
37 |
38 | export const findNZCaseCitation = (query: string): Finder.CaseCitationFinderResult[] => {
39 | const matches = findNZCaseCitationMatches(query)
40 | if (matches.length > 0) {
41 | return matches.map((match) => ({
42 | citation: match[0],
43 | index: match.index,
44 | jurisdiction: Constants.JURISDICTIONS.NZ.id,
45 | })).map(c => ({ ...c, type: `case-citation` }))
46 | }
47 | return []
48 | }
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/SG.ts:
--------------------------------------------------------------------------------
1 | import Constants from '../../Constants'
2 | import { formatAbbrs, sortCasesByVolume } from './utils'
3 | import { SGSTBlongFormatRegex } from 'utils/scraper/SG/STB'
4 |
5 | export const SGSCAbbrs = [
6 | { abbr: `SGCA`, appendum: `(\\(I\\))?` },
7 | { abbr: `SGHC`, appendum: `(R|F|\\(I\\)|\\(A\\))?` },
8 | ]
9 |
10 | export const SGNeutralAbbs = [
11 | ...SGSCAbbrs,
12 | { abbr: `SDRP` },
13 | { abbr: `SGAB` },
14 | { abbr: `SGCAB` },
15 | { abbr: `SGCCS` },
16 | { abbr: `SGCRT` },
17 | { abbr: `SGDC` },
18 | { abbr: `SGFC` },
19 | { abbr: `SGYC` },
20 | { abbr: `SGDSC` },
21 | { abbr: `SGIAC` },
22 | { abbr: `SGIPOS` },
23 | { abbr: `SGITBR` },
24 | { abbr: `SGJC` },
25 | { abbr: `SGMC` },
26 | { abbr: `SGMCA` },
27 | { abbr: `SGMML` },
28 | { abbr: `SGPC` },
29 | { abbr: `SGPDPC` },
30 | { abbr: `SGPDPCR` },
31 | { abbr: `SGSTB` },
32 | ]
33 |
34 | const SGJournalAbbrs = [
35 | { abbr: `SLR`, appendum: `(\\(r\\))?` },
36 | { abbr: `MLR` },
37 | { abbr: `MLJ` },
38 | ]
39 |
40 | export const SGAbbrs = [
41 | ...SGNeutralAbbs,
42 | ...SGJournalAbbrs,
43 | ]
44 |
45 | export const sortSGCases = (
46 | citationsArray: Law.Case[],
47 | attribute: string,
48 | ): Law.Case[] => sortCasesByVolume(
49 | SGAbbrs,
50 | citationsArray,
51 | attribute,
52 | )
53 |
54 | export const makeCaseCitationRegex = (
55 | abbrs: typeof SGAbbrs,
56 | ) => new RegExp(
57 | `(` +
58 | `\\[(?[12]\\d{3})\\]( \\d{1,2})? (?${formatAbbrs(abbrs)
59 | }) \\d{1,4}` +
60 | `|` +
61 | `(?${SGSTBlongFormatRegex.source})` +
62 | `)`, `gi`)
63 |
64 | export const findSGCaseCitationMatches = (query: string) => {
65 | const regex = makeCaseCitationRegex(SGAbbrs)
66 | return [...query.matchAll(regex)]
67 | }
68 |
69 | const getAbbr = (match) => {
70 | if (match.groups.stb) {
71 | return `SGSTB`
72 | }
73 | return match.groups.abbr
74 | }
75 |
76 | export const findSGCaseCitation = (query: string): Finder.CaseCitationFinderResult[] => {
77 | const matches = findSGCaseCitationMatches(query)
78 | return matches.map((match) => ({
79 | abbr: getAbbr(match),
80 | citation: match[0],
81 | index: match.index,
82 | jurisdiction: Constants.JURISDICTIONS.SG.id,
83 | type: `case-citation`,
84 | year: match.groups.year,
85 | }))
86 | }
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/UN.ts:
--------------------------------------------------------------------------------
1 | import Constants from '../../Constants'
2 |
3 | export const sortUNCases = (
4 | citationsArray: Law.Case[],
5 | attribute: string,
6 | ): Law.Case[] => citationsArray
7 |
8 | export const findUNCaseCitation = (
9 | query: string,
10 | ): Finder.CaseCitationFinderResult[] => {
11 | // TODO: figure out ICJ citation format
12 | return []
13 | }
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './CaseCitationFinder'
--------------------------------------------------------------------------------
/src/utils/Finder/CaseCitationFinder/utils.ts:
--------------------------------------------------------------------------------
1 | import Logger from "utils/Logger"
2 |
3 | export const formatAbbr = (
4 | { abbr, appendum = null }:
5 | { abbr: string, appendum?: string },
6 | ) => {
7 | const end = appendum || ``
8 | return `${abbr
9 | .split(``)
10 | .map((letter: string) =>
11 | /[a-z]/i.test(letter)
12 | ? `${letter}\\.?`
13 | : letter,
14 | ).join(``)
15 | }${end}`
16 | }
17 |
18 | export const formatAbbrs = (
19 | abbrArray: { abbr: string, appendum?: string }[],
20 | ) => abbrArray.map(
21 | ({ abbr, appendum }) => formatAbbr({ abbr, appendum }),
22 | ).join(`|`)
23 |
24 | export const cleanVolume = (volumeString: string) => volumeString.replaceAll(`.`, ``).toLowerCase().trim()
25 |
26 | export const sortCitationsByVolume = (
27 | abbrsList: { abbr: string, appendum?: string }[],
28 | citationsArray: string[],
29 | ): string[] => {
30 | const lastIfNotFound = (index: number) => (
31 | index === -1
32 | ? citationsArray.length + 1
33 | : index
34 | )
35 | return citationsArray.sort((a, b) => {
36 | const indexA = lastIfNotFound(
37 | abbrsList.findIndex(
38 | currentAbbr => new RegExp(
39 | formatAbbr(currentAbbr),
40 | `i`,
41 | ).test(a),
42 | ),
43 | )
44 | const indexB = lastIfNotFound(
45 | abbrsList.findIndex(currentAbbr =>
46 | new RegExp(formatAbbr(currentAbbr), `i`).test(b),
47 | ),
48 | )
49 | return indexA === indexB ? (
50 | a.length - b.length
51 | ) : (indexA - indexB)
52 | })
53 | }
54 |
55 | export const sortCasesByVolume = (
56 | abbrsList: { abbr: string, appendum?: string }[],
57 | citationsArray: Law.Case[],
58 | attribute = `citation`,
59 | ): Law.Case[] => sortCitationsByVolume(
60 | abbrsList,
61 | (citationsArray).map(c => c[attribute]),
62 | ).map(c => (citationsArray).find(v => v[attribute] === c))
--------------------------------------------------------------------------------
/src/utils/Finder/Finder.ts:
--------------------------------------------------------------------------------
1 | import Memoize from 'memoizee'
2 | import CaseCitationFinder from './CaseCitationFinder'
3 | import Helpers from 'utils/Helpers'
4 | import Logger from 'utils/Logger'
5 | // import LegislationFinder from './LegislationFinder'
6 |
7 | const findCase = Memoize((citation: string): Finder.FinderResult[] => {
8 | const cleanCitation = Helpers.cleanQuery(citation)
9 | const caseCitations = [...CaseCitationFinder.findCaseCitation(cleanCitation)]
10 | if (caseCitations.length > 0) {
11 | Logger.log(`findCase caseCitations found: `, caseCitations)
12 | return caseCitations
13 | }
14 |
15 | if (cleanCitation.trim().length > 0) {
16 | return [{ name: cleanCitation, type: `case-name` }]
17 | }
18 |
19 | return []
20 | })
21 |
22 | const Finder = {
23 | findCase,
24 | findCaseCitation: CaseCitationFinder.findCaseCitation,
25 | }
26 |
27 | export default Finder
--------------------------------------------------------------------------------
/src/utils/Finder/LegislationFinder.ts:
--------------------------------------------------------------------------------
1 | import Helpers from 'utils/Helpers'
2 | import StatuteAbbrs from './StatuteAbbrs'
3 |
4 | const unabbreviateStatute = (abbrStatute: string) => {
5 | const makeRegex = (string: string) => {
6 | const escapedString = Helpers.escapeRegExp(string)
7 | return (new RegExp(`\\b${escapedString}\\b`, `i`))
8 | }
9 | const isMatch = StatuteAbbrs
10 | .map(abbr => ({ ...abbr, match: abbr.abbrs.find((currentAbbr) => makeRegex(currentAbbr).test(abbrStatute)) }))
11 | .filter(({ match }) => match)
12 |
13 | if(isMatch.length === 0){
14 | return [{ name: abbrStatute }]
15 | }
16 | return isMatch.map(({ name, jurisdiction, match }) => ({
17 | jurisdiction,
18 | name: abbrStatute.replace(makeRegex(match), name),
19 | }))
20 | }
21 |
22 | const provisionAbbreviations = [
23 | {
24 | abbrs: [`s`, `sec`, `section`],
25 | name: `Section`,
26 | },
27 | {
28 | abbrs: [`r`, `rule`],
29 | name: `Rule`,
30 | },
31 | {
32 | abbrs: [`art`, `article`],
33 | name: `Article`,
34 | },
35 | {
36 | abbrs: [`o`, `order`],
37 | name: `Order`,
38 | },
39 | {
40 | abbrs: [`reg`, `regulation`],
41 | name: `Regulation`,
42 | },
43 | ]
44 | const provisionTypeRegex = provisionAbbreviations.map((a) => a.abbrs.join(`|`)).join(`|`)
45 | const provisionSubsectionRegex = new RegExp(`\\(\\d{1,2}[A-Z]{0,2}\\)`)
46 |
47 | const unabbreviateProvision = (provisionType: string) => {
48 | const isMatch = provisionAbbreviations.filter(({ abbrs }) => abbrs.includes(provisionType.trim().toLowerCase()))
49 | if(isMatch.length === 1){
50 | return isMatch[0].name
51 | }
52 | return provisionType
53 | }
54 |
55 | const findLegislation = (citation: string): Finder.LegislationFinderResult[] => {
56 | // eslint-disable-next-line unicorn/better-regex
57 | const regex = new RegExp(`((?(${provisionTypeRegex}) ?\\d{1,4}(${provisionSubsectionRegex.source})*)[ ,]*)?(?[A-Z]{2,}[ ,A-Z]*( ?[12]\\d{3})?)`, `gi`)
58 | const cleanedCitation = citation.trim()
59 | const matches = [...cleanedCitation.matchAll(regex)]
60 | .map(([_1, _2, provision, _3, _4, statute]) => {
61 | if(!provision){
62 | return {
63 | provisionNumber: false,
64 | provisionType: false,
65 | statute,
66 | type: `legislation`,
67 | }
68 | }
69 | const [_, provisionType, provisionNumber] = provision.match(/([a-z]+) ?(\d+[a-z]*)/i)
70 | return {
71 | provisionNumber,
72 | provisionType: unabbreviateProvision(provisionType),
73 | statute,
74 | type: `legislation`,
75 | }
76 | })
77 |
78 | return matches.reduce((current, { statute, ...attributes }) => {
79 | return [
80 | ...current,
81 | ...unabbreviateStatute(statute)
82 | .map(({ name, jurisdiction }) => ({
83 | ...attributes,
84 | ...(jurisdiction ? { jurisdiction } : {}),
85 | statute: name,
86 | })),
87 | ]
88 | }, [])
89 | }
90 |
91 | const LegislationFinder = {
92 | findLegislation,
93 | }
94 |
95 | export default LegislationFinder
--------------------------------------------------------------------------------
/src/utils/Finder/__tests__/CaseCitationFinder.test.ts:
--------------------------------------------------------------------------------
1 | import CaseCitationFinder from '../CaseCitationFinder'
2 |
3 | describe(`Finder`, () => {
4 | const SGCitations = [
5 | `[2021] SGCA 34`,
6 | `[2020] SGHC 228`,
7 | `[2020] SGHC 100 (2010)`,
8 | `[2011] 2 SLR 47`,
9 | `[2020] SGDC 95`,
10 | `[1994] SGCA 128`,
11 | `[1988] SGCA 16`,
12 | `[2001] 2 SLR 253`,
13 | ` [2016] 1 SLR 373 `,
14 | ` [2018] SGHC 22`,
15 | `[2022] SGIPOS 4`,
16 | `[2022] SGHCR 1`,
17 | `[2020] SGHCF 18`,
18 | `[2022] SGHC(A) 17`,
19 | `[2020] SGCA(I) 2`,
20 | ]
21 |
22 | const UKCitations = [
23 | `(1754) 2 Ves Sen 547`,
24 | `[1843-60] All ER 249`,
25 | `[1948] 1 KB 223`,
26 | `[1965] 1 QB 456`,
27 | `(1951) 35 Cr App R 164`,
28 | `[2007] UKHL 17`,
29 | `O/328/22`,
30 | `O/002/22`,
31 | `O01422`,
32 | `BLO/555/22`,
33 | `[2021] EWHC 2168 (QB)`,
34 | `[2016] EWHC 3151 (IPEC)`,
35 | `[2001] ECDR 5`,
36 | ]
37 |
38 | const EUCitations: [string, number][] = [
39 | [`C-40/17`, 1],
40 | [`Cases C-403/08 and C-429/08`, 2],
41 | [`Case C-145/10`, 1],
42 | [`Case C-5/08`, 1],
43 | ]
44 |
45 | const AUCitations = [
46 | `[2010] FCA 984`,
47 | `(1990) 171 CLR 30`,
48 | `(1992) 7 ACSR 759`,
49 | `(2003) 200 ALJ 1`,
50 | `[1989] HCA 9`,
51 | `(1982) 7 ACLR 171`,
52 | ]
53 |
54 | const CACitations = [
55 | `[1999] 2 SCR 534`,
56 | `(2008) 91 OR (3d) 353`,
57 | `(1973) 40 DLR (3d) 371`,
58 | `[1983] 4 WWR 762`,
59 | `[2006] SCC 39`,
60 | `2022 SCC 38 (CanLII)`,
61 | `2008 ONCA 506 (CanLII)`,
62 | ]
63 |
64 | const HKCitations = [
65 | `(2013) 16 HKCFAR 366`,
66 | `[2015] HKCFI 1856`,
67 | `[1999] HKCA 585`,
68 | `[2000] 1 HKLRD 595`,
69 | `[2012] HKDC 911`,
70 | ]
71 |
72 | const citations: [string, string, number][] = [
73 | ...SGCitations.map((citation): [string, string, number] => ([citation, `SG`, 1])),
74 | ...UKCitations.map((citation): [string, string, number] => ([citation, `UK`, 1])),
75 | ...EUCitations.map(([citation, count]): [string, string, number] => ([citation, `EU`, count])),
76 | ...AUCitations.map((citation): [string, string, number] => ([citation, `AU`, 1])),
77 | ...CACitations.map((citation): [string, string, number] => ([citation, `CA`, 1])),
78 | ...HKCitations.map((citation): [string, string, number] => ([citation, `HK`, 1])),
79 | ]
80 |
81 | test.concurrent.each(citations)(`%s`, (citation, jurisdictionId, count) => {
82 | const parsedCitation = CaseCitationFinder.findCaseCitation(citation)
83 | expect(parsedCitation.length).toEqual(count)
84 | expect(parsedCitation[0].jurisdiction).toEqual(jurisdictionId)
85 | expect(parsedCitation).toMatchSnapshot()
86 | })
87 | })
--------------------------------------------------------------------------------
/src/utils/Finder/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Finder'
2 |
--------------------------------------------------------------------------------
/src/utils/Flag.ts:
--------------------------------------------------------------------------------
1 | import { browser } from 'webextension-polyfill-ts'
2 | import Constants from 'utils/Constants'
3 | import AUFlag from 'svg-country-flags/svg/au.svg'
4 | import CAFlag from 'svg-country-flags/svg/ca.svg'
5 | import EUFlag from 'svg-country-flags/svg/eu.svg'
6 | import HKFlag from 'svg-country-flags/svg/hk.svg'
7 | import MYFlag from 'svg-country-flags/svg/my.svg'
8 | import NZFlag from 'svg-country-flags/svg/nz.svg'
9 | import SGFlag from 'svg-country-flags/svg/sg.svg'
10 | import GBFlag from 'svg-country-flags/svg/gb.svg'
11 |
12 | export const getFlagSource = (id: Law.Jurisdiction[`id`]) => {
13 | let source
14 | switch(id){
15 | case Constants.JURISDICTIONS.AU.id: {
16 | source = AUFlag
17 | break
18 | }
19 | case Constants.JURISDICTIONS.CA.id: {
20 | source = CAFlag
21 | break
22 | }
23 | case Constants.JURISDICTIONS.ECHR.id: {
24 | source = `/assets/ecthr.png`
25 | break
26 | }
27 | case Constants.JURISDICTIONS.EU.id: {
28 | source = EUFlag
29 | break
30 | }
31 | case Constants.JURISDICTIONS.HK.id: {
32 | source = HKFlag
33 | break
34 | }
35 | case Constants.JURISDICTIONS.MY.id: {
36 | source = MYFlag
37 | break
38 | }
39 | case Constants.JURISDICTIONS.NZ.id: {
40 | source = NZFlag
41 | break
42 | }
43 | case Constants.JURISDICTIONS.SG.id: {
44 | source = SGFlag
45 | break
46 | }
47 | case Constants.JURISDICTIONS.UK.id: {
48 | source = GBFlag
49 | break
50 | }
51 | case Constants.JURISDICTIONS.UN.id: {
52 | source = `/assets/icj.png`
53 | break
54 | }
55 | default: {
56 | source = ``
57 | break
58 | }
59 | }
60 | return browser.runtime.getURL(source)
61 | }
--------------------------------------------------------------------------------
/src/utils/Logger.ts:
--------------------------------------------------------------------------------
1 | const DEBUG_MODE = process.env.NODE_ENV === `development`
2 |
3 | const log = (...arguments_: unknown[]) => {
4 | if(DEBUG_MODE){
5 | console.log(`Clerkent:`, ...arguments_)
6 | }
7 | }
8 |
9 | const error = (...arguments_: unknown[]) => {
10 | if(DEBUG_MODE){
11 | console.error(`Clerkent:`, ...arguments_)
12 | }
13 | }
14 |
15 | const warn = (...arguments_: unknown[]) => {
16 | if(DEBUG_MODE){
17 | console.warn(`Clerkent:`, ...arguments_)
18 | }
19 | }
20 |
21 | const Logger = {
22 | error,
23 | log,
24 | warn,
25 | }
26 |
27 | export default Logger
--------------------------------------------------------------------------------
/src/utils/Messenger.ts:
--------------------------------------------------------------------------------
1 | const ACTION_TYPES = {
2 | downloadFile: `downloadFile`,
3 | downloadPDF: `downloadPDF`,
4 | lawnetSearch: `lawnetSearch`,
5 | search: `search`,
6 | test: `test`,
7 | test2: `test2`,
8 | viewCitation: `viewCitation`,
9 | }
10 |
11 | const TARGETS = {
12 | background: `background`,
13 | contentScript: `contentScript`,
14 | popup: `popup`,
15 | }
16 |
17 | const Messenger = {
18 | ACTION_TYPES,
19 | TARGETS,
20 | }
21 |
22 | export default Messenger
--------------------------------------------------------------------------------
/src/utils/OptionsStorage.ts:
--------------------------------------------------------------------------------
1 | import Storage from './Storage'
2 |
3 | const keysObject = {
4 | OPTIONS_CLIPBOARD_PASTE_ENABLED: {
5 | defaultValue: false as boolean,
6 | shortName: `clipboardPaste`,
7 | },
8 | OPTIONS_HIGHLIGHT_ENABLED: {
9 | defaultValue: true as boolean,
10 | shortName: `highlight`,
11 | },
12 | OPTIONS_INSTITUTIONAL_LOGIN: {
13 | defaultValue: `None` as InstitutionalLogin,
14 | shortName: `institutionalLogin`,
15 | },
16 | } as const
17 |
18 | type OptionFullKey = keyof typeof keysObject
19 | export type OptionShortName = typeof keysObject[OptionFullKey][`shortName`]
20 | type OptionType = typeof keysObject[T][`defaultValue`]
21 | export type OptionsSettings = {
22 | [K in OptionFullKey]: OptionType
23 | }
24 |
25 | const defaultOptions = Object.entries(keysObject).reduce((accumulator, [key, { defaultValue }]) => ({
26 | ...accumulator,
27 | [key]: defaultValue,
28 | }), {} as OptionsSettings)
29 |
30 | const makeGet = (settingKey: K) => async (): Promise => {
31 | const result = await Storage.get(settingKey)
32 | if(result === null){
33 | return defaultOptions[settingKey]
34 | }
35 | return result
36 | }
37 |
38 | const makeSet = (settingKey: K) => (
39 | value: typeof keysObject[K][`defaultValue`],
40 | ) => Storage.set(
41 | settingKey,
42 | value,
43 | )
44 |
45 | export type OptionStorageContentType = {
46 | [Property in OptionFullKey as typeof keysObject[OptionFullKey][`shortName`]]: {
47 | get: () => Promise>,
48 | set: (value: OptionType) => void,
49 | }
50 | }
51 |
52 | const optionStorageContent = Object.entries(keysObject).reduce((
53 | accumulator,
54 | [key, { shortName }] : [OptionFullKey, typeof keysObject[OptionFullKey]]) => ({
55 | ...accumulator,
56 | [shortName]: {
57 | get: makeGet(key as OptionFullKey),
58 | set: makeSet(key as OptionFullKey),
59 | },
60 | }), {} as OptionStorageContentType)
61 |
62 | const getAll = async () => {
63 | const allResults = await Promise.all(
64 | Object.values(keysObject).map(({ shortName }) => optionStorageContent[shortName].get()),
65 | )
66 |
67 | return Object.entries(keysObject).reduce((accumulator, [key], index) => ({
68 | ...accumulator,
69 | [key]: allResults[index],
70 | }), {} as OptionsSettings)
71 | }
72 |
73 | const OptionsStorage = {
74 | ...optionStorageContent,
75 | defaultOptions,
76 | getAll,
77 | }
78 |
79 | export default OptionsStorage
--------------------------------------------------------------------------------
/src/utils/PDF.ts:
--------------------------------------------------------------------------------
1 | import { browser } from "webextension-polyfill-ts"
2 | import Browser from './Browser'
3 |
4 | const saveFirefox = (filename: string) => browser.tabs.saveAsPDF({
5 | footerLeft: ``,
6 | footerRight: ``,
7 | headerLeft: ``,
8 | headerRight: ``,
9 | toFileName: filename,
10 | })
11 |
12 | const save = async ({
13 | url,
14 | code,
15 | fileName,
16 | }) => {
17 | const isFirefox = Browser.isFirefox()
18 | const isChrome = Browser.isChrome()
19 |
20 | const tab = await browser.tabs.create({ url: url })
21 | await browser.tabs.executeScript(tab.id, {
22 | code: code +`document.title=\`${fileName}\`; ${
23 | isChrome ? `window.setTimeout(() => window.print(), 0);` : ``
24 | }`,
25 | })
26 |
27 | if(isFirefox){
28 | await saveFirefox(fileName)
29 | await browser.tabs.remove(tab.id)
30 | }
31 | // if(isChrome){
32 | // await saveChrome()
33 | // }
34 | }
35 |
36 | const PDF = {
37 | save,
38 | }
39 |
40 | export default PDF
--------------------------------------------------------------------------------
/src/utils/Request.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { buildWebStorage, setupCache } from 'axios-cache-interceptor'
3 |
4 | const storage = buildWebStorage(localStorage, `request-cache:`)
5 |
6 | const request = setupCache(
7 | axios.create({
8 | timeout: 10_000,
9 | }),
10 | {
11 | methods: [`get`, `post`],
12 | storage,
13 | ttl: 1000 * 60 * 60, // an hour
14 | },
15 | )
16 |
17 | export default request
--------------------------------------------------------------------------------
/src/utils/Storage.ts:
--------------------------------------------------------------------------------
1 | import { browser } from 'webextension-polyfill-ts'
2 | import Logger from './Logger'
3 |
4 | const set = (key: string, value: unknown, noJSON = false) => {
5 | Logger.log(`Storage storing`, key, value)
6 | if(noJSON){
7 | return browser.storage.local.set({
8 | [key]: value,
9 | })
10 | }
11 | return browser.storage.local.set({
12 | [key]: JSON.stringify(value),
13 | })
14 | }
15 | const get = async (key: string, noJSON = false) => {
16 | const result = await browser.storage.local.get({[key]: null})
17 | if(result === null){
18 | return null
19 | }
20 | Logger.log(`Storage retrieving`, key, result[key])
21 | if(noJSON){
22 | return result[key]
23 | }
24 | return JSON.parse(result[key])
25 | }
26 |
27 | const clear = () => browser.storage.local.clear()
28 |
29 | const remove = (key: string) => browser.storage.local.remove(key)
30 |
31 | const Storage = {
32 | clear,
33 | get,
34 | remove,
35 | set,
36 | }
37 |
38 | export default Storage
--------------------------------------------------------------------------------
/src/utils/__tests__/Helpers.test.ts:
--------------------------------------------------------------------------------
1 | import Helpers from '../Helpers'
2 |
3 | const commonAppendsTestCases = [
4 | [`Altus Technologies Pte Ltd (under judicial management) v Oversea-Chinese Banking Corp Ltd`, `Altus Technologies v Oversea-Chinese Banking Corp`],
5 | [`Kong Chee Chui and others v Soh Ghee Hong`, `Kong Chee Chui v Soh Ghee Hong`],
6 | [`Fustar Chemicals Ltd v Ong Soo Hwa (liquidator of Fustar Chemicals Pte Ltd)`, `Fustar Chemicals v Ong Soo Hwa (liquidator of Fustar Chemicals)`],
7 | [`Good Property Land Development Pte Ltd (in liquidation) v Societe-Generale`, `Good Property Land Development v Societe-Generale`],
8 | [`G.W.L. Properties Ltd. v. W.R. Grace & Co. of Canada Ltd.`, `G.W.L. Properties v. W.R. Grace of Canada`],
9 | [`PEX International Pte Ltd v Lim Seng Chye & Anor`, `PEX International v Lim Seng Chye`],
10 | [`SCK SERIJADI SDN BHD v ARTISON INTERIOR PTE LTD`, `SCK SERIJADI v ARTISON INTERIOR`],
11 | [`Wong Kwei Cheong v ABN-AMRO Bank NV`, `Wong Kwei Cheong v ABN-AMRO Bank`],
12 | [`Wham Kwok Han Jolovan v AG`, `Wham Kwok Han Jolovan v AG`],
13 | ]
14 |
15 | describe(`Helpers`, () => {
16 | test.concurrent
17 | .each(commonAppendsTestCases)(`removeCommonAppends: %s`, (input, output) => {
18 | const processed = Helpers.removeCommonAppends(input)
19 | expect(processed).toBe(output)
20 | })
21 | })
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Constants } from './Constants'
2 | export { default as Finder } from './Finder'
3 | export { default as Messenger } from './Messenger'
4 | export { default as Request } from './Request'
5 | export { default as Storage } from './Storage'
6 | export { default as Logger } from './Logger'
7 | export { default as Helpers } from './Helpers'
8 | export { default as Clipboard } from './Clipboard'
--------------------------------------------------------------------------------
/src/utils/scraper/AU/FCA.ts:
--------------------------------------------------------------------------------
1 | import { CacheRequestConfig } from "axios-cache-interceptor"
2 | import * as cheerio from 'cheerio'
3 | import { Constants, Request } from "utils"
4 | import CaseCitationFinder from "utils/Finder/CaseCitationFinder"
5 |
6 | const search = async (query: string): Promise => {
7 | const { data } = await Request.get(`https://search2.fedcourt.gov.au/s/search.html`, {
8 | params: {
9 | collection: `judgments`,
10 | meta_2: query,
11 | meta_v_phrase_orsand: `judgments/Judgments/`,
12 | sort: `date`,
13 | },
14 | } as CacheRequestConfig)
15 |
16 | const $ = cheerio.load(data)
17 |
18 | return $(`#fb-results > .result`).map((_, element): Law.Case => {
19 | const title = $(`h3 > a`, element).text().trim()
20 | const citation = CaseCitationFinder.findAUCaseCitation(title)[0].citation
21 | const name = title.replace(citation, ``)
22 | const url = $(`h3 > a`, element).attr(`href`)
23 | return {
24 | citation,
25 | database: Constants.DATABASES.AU_hca,
26 | jurisdiction: Constants.JURISDICTIONS.AU.id,
27 | links: [
28 | {
29 | doctype: `Summary`,
30 | filetype: `HTML`,
31 | url,
32 | },
33 | ],
34 | name,
35 | }
36 | }).get()
37 | }
38 |
39 | const getCaseByCitation = async (citation: string): Promise => search(citation)
40 |
41 | const getCaseByName = async (caseName: string): Promise => search(caseName)
42 |
43 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
44 | return null
45 | }
46 |
47 | const FCA = {
48 | getCaseByCitation,
49 | getCaseByName,
50 | getPDF,
51 | }
52 |
53 | export default FCA
--------------------------------------------------------------------------------
/src/utils/scraper/AU/HCA.ts:
--------------------------------------------------------------------------------
1 | import qs from 'qs'
2 | import * as cheerio from 'cheerio'
3 | import Request from 'utils/Request'
4 | import CaseCitationFinder from 'utils/Finder/CaseCitationFinder'
5 | import Constants from 'utils/Constants'
6 | import type { CacheRequestConfig } from 'axios-cache-interceptor'
7 | import Helpers from 'utils/Helpers'
8 |
9 | const DOMAIN = `https://eresources.hcourt.gov.au`
10 | const NOT_FOUND_STRING = `The case could not be found on the database.`
11 |
12 | const getCaseByCitation = async (citation: string): Promise => {
13 | const [{ year, page }] = CaseCitationFinder.findAUCaseCitation(citation)
14 | const url = `${DOMAIN}/showCase/${year}/HCA/${page}`
15 | const { data } = await Request.get(url)
16 |
17 | const $ = cheerio.load(data)
18 |
19 | const notFoundElement = $(`.wellCase`)
20 |
21 | if(notFoundElement.text().trim() === NOT_FOUND_STRING){
22 | return []
23 | }
24 |
25 | const name = $(`title`).text().trim()
26 |
27 | return [{
28 | citation,
29 | database: Constants.DATABASES.AU_hca,
30 | jurisdiction: Constants.JURISDICTIONS.AU.id,
31 | links: [
32 | {
33 | doctype: `Summary`,
34 | filetype: `HTML`,
35 | url,
36 | },
37 | {
38 | doctype: `Judgment`,
39 | filetype: `PDF`,
40 | url: url.replace(`showCase`, `downloadPdf`),
41 | },
42 | ],
43 | name,
44 | }]
45 | }
46 |
47 | const getCaseByName = async (caseName: string): Promise => {
48 | const { data } = await Request.get(
49 | `${DOMAIN}/browse`,
50 | {
51 | params: {
52 | col: 0,
53 | facets: `name`,
54 | 'srch-term': caseName,
55 | },
56 | paramsSerializer: {
57 | serialize: (parameters: Record) => qs.stringify(parameters, { format : `RFC1738` }),
58 | },
59 | } as CacheRequestConfig,
60 | )
61 |
62 | const $ = cheerio.load(data)
63 | const results = $(`#caseList > h4`).map((_, element): Law.Case => {
64 | const name = $(`strong`, element).text().trim()
65 | const citation = $(`span`, element).text().trim()
66 | const url = DOMAIN + $(`a`, element).attr(`href`)
67 |
68 | const [{ year, page }] = CaseCitationFinder.findAUCaseCitation(citation)
69 |
70 | return {
71 | citation,
72 | database: Constants.DATABASES.AU_hca,
73 | jurisdiction: Constants.JURISDICTIONS.AU.id,
74 | links: [
75 | {
76 | doctype: `Summary`,
77 | filetype: `HTML`,
78 | url,
79 | },
80 | {
81 | doctype: `Judgment`,
82 | filetype: `PDF`,
83 | url: `${DOMAIN}/downloadPdf/${year}/HCA/${page}`,
84 | },
85 | ],
86 | name,
87 | }
88 | }).get()
89 |
90 | return results.filter(({ citation }) => Helpers.isCitationValid(citation))
91 | }
92 |
93 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
94 | return null
95 | }
96 |
97 | const HCA = {
98 | getCaseByCitation,
99 | getCaseByName,
100 | getPDF,
101 | }
102 |
103 | export default HCA
--------------------------------------------------------------------------------
/src/utils/scraper/AU/QueenslandJudgments.ts:
--------------------------------------------------------------------------------
1 | import * as cheerio from 'cheerio'
2 | import Request from 'utils/Request'
3 | import Constants from 'utils/Constants'
4 | import Helpers from 'utils/Helpers'
5 | import { findCitation } from '../utils'
6 | import { findAUCaseCitation } from 'utils/Finder/CaseCitationFinder/AU'
7 | import { CacheRequestConfig } from 'axios-cache-interceptor'
8 |
9 | const DOMAIN = `https://www.queenslandjudgments.com.au`
10 |
11 | const parseCaseResults = (htmlResponse: string): Law.Case[] => {
12 | const $ = cheerio.load(htmlResponse)
13 | const results = $(`#home > ul.result-list > li`).map((_, element): Law.Case => {
14 | const title = $(`span.caseName`, element).text().trim()
15 | const citation = findCitation(findAUCaseCitation, title)
16 | const name = title.replace(citation, ``).trim()
17 | const path = $(`.enhanced-search > .custom-tooltip:nth-of-type(1) > a`, element).attr(`href`).replace(/\?.*$/, ``)
18 | const link = `${DOMAIN}${path}`
19 | return {
20 | citation,
21 | database: Constants.DATABASES.AU_queensland_judgments,
22 | jurisdiction: Constants.JURISDICTIONS.AU.id,
23 | links: [
24 | {
25 | doctype: `Summary`,
26 | filetype: `HTML`,
27 | url: link,
28 | },
29 | {
30 | doctype: `Judgment`,
31 | filetype: `PDF`,
32 | url: `${link}/pdf`,
33 | },
34 | ],
35 | name,
36 | }
37 | }).get()
38 | return results.filter(({ citation }) => Helpers.isCitationValid(citation))
39 | }
40 |
41 | const getCaseByCitation = async (citation: string): Promise => {
42 | const { data } = await Request.get(
43 | `${DOMAIN}/caselaw-search/query`,
44 | {
45 | params: {
46 | queryStringCitation: citation,
47 | requestSource: `quickCitationSearch`,
48 | tab: `citation`,
49 | },
50 | } as CacheRequestConfig,
51 | )
52 | return parseCaseResults(data)
53 | }
54 |
55 | const getCaseByName = async (caseName: string): Promise => {
56 | const { data } = await Request.get(
57 | `${DOMAIN}/caselaw-search/query`,
58 | {
59 | params: {
60 | queryStringCaseName: caseName,
61 | requestSource: `quickCaseNameSearch`,
62 | tab: `case-name`,
63 | },
64 | } as CacheRequestConfig,
65 | )
66 | return parseCaseResults(data)
67 | }
68 |
69 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
70 | return null
71 | }
72 |
73 | const QueenslandJudgments = {
74 | getCaseByCitation,
75 | getCaseByName,
76 | getPDF,
77 | }
78 |
79 | export default QueenslandJudgments
--------------------------------------------------------------------------------
/src/utils/scraper/AU/QueenslandSCL.ts:
--------------------------------------------------------------------------------
1 | import * as cheerio from 'cheerio'
2 | import Request from 'utils/Request'
3 | import Constants from 'utils/Constants'
4 | import Helpers from 'utils/Helpers'
5 | import { CacheRequestConfig } from 'axios-cache-interceptor'
6 |
7 | const API_DOMAIN = `https://api.sclqld.org.au`
8 | const DOMAIN = `https://www.sclqld.org.au`
9 |
10 | const defaultParameters = {
11 | c: ``,
12 | court: 0,
13 | cw: ``,
14 | fn: ``,
15 | form_id: `caselaw_advanced_search_form`,
16 | format: `results`,
17 | j: ``,
18 | lg: ``,
19 | lpp: 25,
20 | op: `Search`,
21 | p: 0,
22 | pt: ``,
23 | q: ``,
24 | reported: 0,
25 | sort: `score:desc`,
26 | }
27 |
28 | const parseCaseResults = (htmlResponse: string): Law.Case[] => {
29 | const $ = cheerio.load(htmlResponse)
30 | const results = $(`#results-list > tbody > tr`).map((_, element): Law.Case => {
31 | const nameElement = $(`a.qjudgment`, element)
32 | const name = Helpers.htmlDecode(nameElement.text()).trim()
33 | const path = nameElement.attr(`href`)
34 | const link = `${DOMAIN}${path}`
35 | const citation = $(`span.no-wrap:first-of-type`, element).text().trim()
36 | const pdfLink = $(`a.direct-link`, element).attr(`href`)
37 | return {
38 | citation,
39 | database: Constants.DATABASES.AU_queensland_scl,
40 | jurisdiction: Constants.JURISDICTIONS.AU.id,
41 | links: [
42 | {
43 | doctype: `Summary`,
44 | filetype: `HTML`,
45 | url: link,
46 | },
47 | {
48 | doctype: `Judgment`,
49 | filetype: `PDF`,
50 | url: pdfLink,
51 | },
52 | ],
53 | name,
54 | }
55 | }).get()
56 | return results.filter(({ citation }) => Helpers.isCitationValid(citation))
57 | }
58 |
59 | const getCaseByCitation = async (citation: string): Promise => {
60 | const { data } = await Request.get(
61 | `${API_DOMAIN}/v1/caselaw/search`,
62 | {
63 | params: {
64 | ...defaultParameters,
65 | c: citation,
66 | },
67 | } as CacheRequestConfig,
68 | )
69 | return parseCaseResults(data)
70 | }
71 |
72 | const getCaseByName = async (caseName: string): Promise => {
73 | const { data } = await Request.get(
74 | `${API_DOMAIN}/v1/caselaw/search`,
75 | {
76 | params: {
77 | ...defaultParameters,
78 | pt: caseName,
79 | },
80 | } as CacheRequestConfig,
81 | )
82 | return parseCaseResults(data)
83 | }
84 |
85 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
86 | return null
87 | }
88 |
89 | const QueenslandSCL = {
90 | getCaseByCitation,
91 | getCaseByName,
92 | getPDF,
93 | }
94 |
95 | export default QueenslandSCL
--------------------------------------------------------------------------------
/src/utils/scraper/AU/VictoriaLawLibrary.ts:
--------------------------------------------------------------------------------
1 | import { CacheRequestConfig } from 'axios-cache-interceptor'
2 | import * as cheerio from 'cheerio'
3 | import Constants from 'utils/Constants'
4 | import { findAUCaseCitation } from 'utils/Finder/CaseCitationFinder/AU'
5 | import Helpers from 'utils/Helpers'
6 | import Request from 'utils/Request'
7 | import { findCitation } from '../utils'
8 |
9 | const DOMAIN = `https://www.lawlibrary.vic.gov.au`
10 |
11 | const parseCaseResults = (htmlResponse: string): Law.Case[] => {
12 | const $ = cheerio.load(htmlResponse)
13 | const results = $(`.view.view-search .view-content .views-row`).map((_, element): Law.Case => {
14 | const titleElement = $(`a`, element)
15 | const textContent = titleElement.text().trim()
16 | const citation = findCitation(findAUCaseCitation, textContent)
17 | const name = textContent.replace(citation, ``).trim()
18 | const path = titleElement.attr(`href`)
19 | const link = `${DOMAIN}${path}`
20 | return {
21 | citation,
22 | database: Constants.DATABASES.AU_victoria_lawlibrary,
23 | jurisdiction: Constants.JURISDICTIONS.AU.id,
24 | links: [
25 | {
26 | doctype: `Judgment`,
27 | filetype: `HTML`,
28 | url: link,
29 | },
30 | ],
31 | name,
32 | }
33 | }).get()
34 | return results.filter(({ citation }) => Helpers.isCitationValid(citation))
35 | }
36 |
37 | const getCaseByCitation = async (citation: string): Promise => {
38 | const { data } = await Request.get(
39 | `${DOMAIN}/search/judgments`,
40 | {
41 | params: {
42 | keywords: citation,
43 | },
44 | } as CacheRequestConfig,
45 | )
46 | return parseCaseResults(data)
47 | }
48 |
49 | const getCaseByName = async (caseName: string): Promise => {
50 | const { data } = await Request.get(
51 | `${DOMAIN}/search/judgments`,
52 | {
53 | params: {
54 | keywords: caseName,
55 | },
56 | } as CacheRequestConfig,
57 | )
58 | return parseCaseResults(data)
59 | }
60 |
61 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
62 | const { data } = await Request.get(Helpers.getJudgmentLink(inputCase.links)?.url)
63 |
64 | const $ = cheerio.load(data)
65 | const pdfURL = $(`.field-name-field-citation a`)
66 | return pdfURL.attr(`href`)
67 | }
68 |
69 | const VictoriaLawLibrary = {
70 | getCaseByCitation,
71 | getCaseByName,
72 | getPDF,
73 | }
74 |
75 | export default VictoriaLawLibrary
--------------------------------------------------------------------------------
/src/utils/scraper/AU/WAECourts.ts:
--------------------------------------------------------------------------------
1 | import Request from 'utils/Request'
2 |
3 | const getCaseByCitation = async (citation: string): Promise => {
4 | return []
5 | }
6 |
7 | const getCaseByName = async (citation: string): Promise => {
8 | return []
9 | }
10 |
11 | const WAECourts = {
12 | getCaseByCitation,
13 | getCaseByName,
14 | }
15 |
16 | export default WAECourts
--------------------------------------------------------------------------------
/src/utils/scraper/AU/austlii.ts:
--------------------------------------------------------------------------------
1 | import * as cheerio from 'cheerio'
2 | import Request from '../../Request'
3 | import Constants from '../../Constants'
4 | import Helpers from '../../Helpers'
5 | import { findAUCaseCitation } from '../../Finder/CaseCitationFinder/AU'
6 | import type { AxiosResponse } from 'axios'
7 | import Logger from '../../Logger'
8 | import { findCitation } from '../utils'
9 | import { CacheRequestConfig } from 'axios-cache-interceptor'
10 |
11 | const DOMAIN = `http://www8.austlii.edu.au`
12 |
13 | const parseCaseData = (data: AxiosResponse[`data`]): Law.Case[] => {
14 | const $ = cheerio.load(data)
15 | return $(`#page-main > .card > ul > li`).map((_, element): Law.Case => {
16 | const name = $(`> a:first-of-type`, element).text().trim()
17 | const path = $(`> a:first-of-type`, element).attr(`href`)
18 | const link = `${DOMAIN}${path}`
19 | const citation = findCitation(findAUCaseCitation, name)
20 | return {
21 | citation,
22 | database: Constants.DATABASES.AU_austlii,
23 | jurisdiction: Constants.JURISDICTIONS.AU.id,
24 | links: [
25 | {
26 | doctype: `Judgment`,
27 | filetype: `HTML`,
28 | url: link,
29 | },
30 | ],
31 | name: name.replace(citation, ``).trim(),
32 | }
33 | }).get()
34 | .filter(({ citation }) => Helpers.isCitationValid(citation))
35 | }
36 |
37 | const getCaseByCitation = async (citation: string): Promise => {
38 | const queryString = `mask_path=au/journals&mask_path=au/cases&mask_path=au/cases/cth`
39 | const { data } = await Request.get(
40 | `${DOMAIN}/cgi-bin/sinosrch.cgi?${queryString}`,
41 | {
42 | params: {
43 | method: `auto`,
44 | query: citation,
45 | },
46 | } as CacheRequestConfig,
47 | )
48 |
49 | return parseCaseData(data)
50 | }
51 |
52 | const getCaseByName = async (caseName: string): Promise => {
53 | const queryString = `mask_path=au/journals&mask_path=au/cases&mask_path=au/cases/cth`
54 | const { data } = await Request.get(
55 | `${DOMAIN}/cgi-bin/sinosrch.cgi?${queryString}`,
56 | {
57 | params: {
58 | method: `auto`,
59 | query: caseName,
60 | },
61 | } as CacheRequestConfig,
62 | )
63 |
64 | return parseCaseData(data)
65 | }
66 |
67 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
68 | const judgmentLink = Helpers.getJudgmentLink(inputCase.links)?.url
69 | const pdfURL = judgmentLink
70 | .replace(/\/(viewdoc|sinodisp)\//, `/sign.cgi/`)
71 | .replace(/\.html\??.*/, ``)
72 | try {
73 | const { request } = await Request.head(pdfURL)
74 | return request.responseURL
75 | } catch {
76 | Logger.log(`austlii: getPDF - no PDF available`)
77 | }
78 |
79 | return null
80 | }
81 |
82 | const AU = {
83 | getCaseByCitation,
84 | getCaseByName,
85 | getPDF,
86 | }
87 |
88 | export default AU
--------------------------------------------------------------------------------
/src/utils/scraper/AU/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './AU'
--------------------------------------------------------------------------------
/src/utils/scraper/CA/CA.ts:
--------------------------------------------------------------------------------
1 | import canlii from './canlii'
2 | import Common from '../common'
3 | import Constants from '../../Constants'
4 | import { sortCACases } from '../../Finder/CaseCitationFinder/CA'
5 | import { databaseUseDatabase, databaseUseJurisdiction, makeEventTarget } from '../utils'
6 |
7 | const databaseUseCA = databaseUseJurisdiction(`CA`)
8 | const databaseUseCanlii = databaseUseDatabase(`canlii`, databaseUseCA)
9 | const databaseUseCommonLII = databaseUseDatabase(`commonlii`, databaseUseCA)
10 |
11 | const getCaseByName = (caseName: string): EventTarget => makeEventTarget(
12 | caseName,
13 | [
14 | databaseUseCanlii(() => canlii.getCaseByName(caseName)),
15 | databaseUseCommonLII(() => Common.CommonLII.getCaseByName(caseName, Constants.JURISDICTIONS.CA.name)),
16 | ],
17 | `CA`,
18 | sortCACases,
19 | true,
20 | )
21 |
22 | const getCaseByCitation = (citation: string ): EventTarget => makeEventTarget(
23 | citation,
24 | [
25 | databaseUseCanlii(() => canlii.getCaseByCitation(citation)),
26 | databaseUseCommonLII(() => Common.CommonLII.getCaseByCitation(citation)),
27 | ],
28 | `CA`,
29 | sortCACases,
30 | false,
31 | )
32 |
33 | const databaseMap = {
34 | [Constants.DATABASES.CA_canlii.id]: canlii,
35 | [Constants.DATABASES.commonlii.id]: Common.CommonLII,
36 | }
37 |
38 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
39 | const { database } = inputCase
40 | return databaseMap[database.id].getPDF(inputCase, inputDocumentType)
41 | }
42 |
43 | const CA = {
44 | getCaseByCitation,
45 | getCaseByName,
46 | getPDF,
47 | }
48 |
49 | export default CA
--------------------------------------------------------------------------------
/src/utils/scraper/CA/canlii.ts:
--------------------------------------------------------------------------------
1 | import * as cheerio from 'cheerio'
2 | import qs from 'qs'
3 | import Request from '../../Request'
4 | import Constants from '../../Constants'
5 | import Helpers from '../../Helpers'
6 | import { findCACaseCitation } from '../../Finder/CaseCitationFinder/CA'
7 | import type { AxiosResponse } from 'axios'
8 | import Logger from '../../Logger'
9 | import PDF from '../../PDF'
10 | import { findCitation } from '../utils'
11 | import { CacheRequestConfig } from 'axios-cache-interceptor'
12 |
13 | const DOMAIN = `https://www.canlii.org`
14 |
15 | const parseCase = (data: AxiosResponse[`data`]): Law.Case[] => {
16 | const { results } = data
17 | return results.map(({
18 | title,
19 | path,
20 | reference,
21 | }): Law.Case => {
22 | const cleanReference = typeof reference === `string`
23 | ? findCitation(
24 | findCACaseCitation,
25 | cheerio.load(reference)(`html`).text().trim(),
26 | ) : ``
27 |
28 | return {
29 | citation: cleanReference,
30 | database: Constants.DATABASES.CA_canlii,
31 | jurisdiction: Constants.JURISDICTIONS.CA.id,
32 | links: [
33 | {
34 | doctype: `Judgment`,
35 | filetype: `HTML`,
36 | url: `${DOMAIN}${path}`,
37 | },
38 | ],
39 | name: cheerio.load(title)(`html`).text().trim(),
40 | }
41 | }).filter(({ citation }) => Helpers.isCitationValid(citation))
42 | }
43 |
44 | const getCaseByCitation = async (citation: string): Promise => {
45 | const { data } = await Request.get(
46 | `${DOMAIN}/en/search/ajaxSearch.do?${qs.stringify({ id: citation, page: 1})}`,
47 | {
48 | headers: {
49 | 'X-Requested-With': `XMLHttpRequest`,
50 | },
51 | } as unknown as CacheRequestConfig,
52 | )
53 |
54 | return parseCase(data)
55 | }
56 |
57 | const getCaseByName = async (caseName: string): Promise => {
58 | const { data } = await Request.get(
59 | `${DOMAIN}/en/search/ajaxSearch.do?${qs.stringify({ id: caseName, page: 1})}`,
60 | {
61 | headers: {
62 | 'X-Requested-With': `XMLHttpRequest`,
63 | },
64 | } as unknown as CacheRequestConfig,
65 | )
66 |
67 | return parseCase(data)
68 | }
69 |
70 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
71 | const judgmentLink = Helpers.getJudgmentLink(inputCase.links)?.url
72 | try {
73 | const { request } = await Request.head(judgmentLink.replace(/\.html$/i, `.pdf`))
74 | return request.responseURL
75 | } catch {
76 | Logger.log(`CANLII: getPDF - no PDF available`)
77 | }
78 |
79 | const fileName = Helpers.getFileName(inputCase, inputDocumentType)
80 | await PDF.save({
81 | code: `document.body.innerHTML = document.querySelector('#originalDocument').innerHTML;`,
82 | fileName,
83 | url: judgmentLink,
84 | })
85 | return null
86 | }
87 |
88 | const CA = {
89 | getCaseByCitation,
90 | getCaseByName,
91 | getPDF,
92 | }
93 |
94 | export default CA
--------------------------------------------------------------------------------
/src/utils/scraper/CA/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './CA'
--------------------------------------------------------------------------------
/src/utils/scraper/CA/scclexum.ts:
--------------------------------------------------------------------------------
1 | import { Request } from "../.."
2 | import Helpers from "../../Helpers"
3 | import Logger from "../../Logger"
4 |
5 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
6 | const judgmentLink = Helpers.getJudgmentLink(inputCase.links)?.url
7 | try {
8 | const { request: initialRequest } = await Request.head(judgmentLink
9 | .replace(/^http:/, `https:`),
10 | )
11 | const updatedLink = initialRequest.responseURL
12 | const pdfURL = updatedLink
13 | .replace(/\/en\/item\//, `/en/`)
14 | .replace(/\/index\.do/, `/1/document.do`)
15 | const { request } = await Request.head(pdfURL)
16 | return request.responseURL
17 | } catch {
18 | Logger.log(`scc-lexum: getPDF - no PDF available`)
19 | }
20 |
21 | return null
22 | }
23 |
24 | const scclexum = {
25 | getPDF,
26 | }
27 |
28 | export default scclexum
--------------------------------------------------------------------------------
/src/utils/scraper/ECHR/ECHR.ts:
--------------------------------------------------------------------------------
1 |
2 | import HUDOC from './HUDOC'
3 | import Constants from '../../Constants'
4 | import { databaseUseDatabase, databaseUseJurisdiction, makeEventTarget } from '../utils'
5 | import { sortECHRCases } from 'utils/Finder/CaseCitationFinder/ECHR'
6 |
7 | const databaseUseECHR = databaseUseJurisdiction(`ECHR`)
8 | const databaseUseHUDOC = databaseUseDatabase(`hudoc`, databaseUseECHR)
9 |
10 | const getCaseByName = (caseName: string): EventTarget => makeEventTarget(
11 | caseName,
12 | [
13 | databaseUseHUDOC(() => HUDOC.getCaseByName(caseName)),
14 | ],
15 | `ECHR`,
16 | sortECHRCases,
17 | true,
18 | )
19 |
20 | const getCaseByCitation = (
21 | citation: string,
22 | ): EventTarget => makeEventTarget(
23 | citation,
24 | [
25 | databaseUseHUDOC(() => HUDOC.getCaseByCitation(citation)),
26 | ],
27 | `ECHR`,
28 | sortECHRCases,
29 | false,
30 | )
31 |
32 | const databaseMap = {
33 | [Constants.DATABASES.ECHR_hudoc.id]: HUDOC,
34 | }
35 |
36 | const getPDF = async (
37 | inputCase: Law.Case,
38 | inputDocumentType: Law.Link[`doctype`],
39 | ): Promise => {
40 | const { database } = inputCase
41 | return databaseMap[database.id].getPDF(inputCase, inputDocumentType)
42 | }
43 |
44 | const ECHR = {
45 | getCaseByCitation,
46 | getCaseByName,
47 | getPDF,
48 | }
49 |
50 | export default ECHR
--------------------------------------------------------------------------------
/src/utils/scraper/ECHR/index.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export {default} from './ECHR'
--------------------------------------------------------------------------------
/src/utils/scraper/EU/EU.ts:
--------------------------------------------------------------------------------
1 | import CURIA from './CURIA'
2 | import EPO from './EPO'
3 | import Constants from '../../Constants'
4 | import { databaseUseDatabase, databaseUseJurisdiction, makeEventTarget } from '../utils'
5 | import { sortEUCases } from 'utils/Finder/CaseCitationFinder/EU'
6 |
7 | const databaseUseEU = databaseUseJurisdiction(`EU`)
8 | const databaseUseCURIA = databaseUseDatabase(`curia`, databaseUseEU)
9 | const databaseUseEPO = databaseUseDatabase(`epo`, databaseUseEU)
10 |
11 | const getCaseByName = (caseName: string): EventTarget => makeEventTarget(
12 | caseName,
13 | [
14 | databaseUseCURIA(() => CURIA.getCaseByName(caseName)),
15 | ],
16 | `EU`,
17 | sortEUCases,
18 | true,
19 | )
20 |
21 | const getCaseByCitation = (citation: string, court: string): EventTarget => makeEventTarget(
22 | citation,
23 | court === `EPO` ? [
24 | databaseUseEPO(() => EPO.getCaseByCitation(citation)),
25 | ] : [
26 | databaseUseCURIA(() => CURIA.getCaseByCitation(citation)),
27 | ],
28 | `EU`,
29 | sortEUCases,
30 | false,
31 | )
32 |
33 | const databaseMap = {
34 | [Constants.DATABASES.EU_curia.id]: CURIA,
35 | [Constants.DATABASES.EU_epo.id]: EPO,
36 | }
37 |
38 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
39 | const { database } = inputCase
40 | return databaseMap[database.id].getPDF(inputCase, inputDocumentType)
41 | }
42 |
43 | const EU = {
44 | getCaseByCitation,
45 | getCaseByName,
46 | getPDF,
47 | }
48 |
49 | export default EU
--------------------------------------------------------------------------------
/src/utils/scraper/EU/index.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export {default} from './EU'
--------------------------------------------------------------------------------
/src/utils/scraper/HK/HK.ts:
--------------------------------------------------------------------------------
1 | import HKLIIORG from './HKLIIORG'
2 | import LRS from './LRS'
3 | import Common from '../common'
4 | import Constants from '../../Constants'
5 | import { sortHKCases } from '../../Finder/CaseCitationFinder/HK'
6 | import { databaseUseDatabase, databaseUseJurisdiction, makeEventTarget } from '../utils'
7 |
8 | const databaseUseHK = databaseUseJurisdiction(`HK`)
9 | const databaseUseLRS = databaseUseDatabase(`lrs`, databaseUseHK)
10 | const databaseUseHKLIIIORG = databaseUseDatabase(`hkliiorg`, databaseUseHK)
11 | const databaseUseCommonLII = databaseUseDatabase(`commonlii`, databaseUseHK)
12 |
13 | const getCaseByName = (caseName: string): EventTarget => makeEventTarget(
14 | caseName,
15 | [
16 | databaseUseLRS(() => LRS.getCaseByName(caseName)),
17 | databaseUseHKLIIIORG(() => HKLIIORG.getCaseByName(caseName)),
18 | databaseUseCommonLII(() => Common.CommonLII.getCaseByName(caseName, Constants.JURISDICTIONS.HK.name)),
19 | ],
20 | `HK`,
21 | sortHKCases,
22 | true,
23 | )
24 |
25 | const getCaseByCitation = (citation: string): EventTarget => makeEventTarget(
26 | citation,
27 | [
28 | databaseUseLRS(() => LRS.getCaseByCitation(citation)),
29 | databaseUseHKLIIIORG(() => HKLIIORG.getCaseByCitation(citation)),
30 | databaseUseCommonLII(() => Common.CommonLII.getCaseByCitation(citation)),
31 | ],
32 | `HK`,
33 | sortHKCases,
34 | false,
35 | )
36 |
37 | const databaseMap = {
38 | [Constants.DATABASES.HK_lrs.id]: LRS,
39 | [Constants.DATABASES.HK_hkliiorg.id]: HKLIIORG,
40 | [Constants.DATABASES.commonlii.id]: Common.CommonLII,
41 | }
42 |
43 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
44 | const { database } = inputCase
45 | return databaseMap[database.id].getPDF(inputCase, inputDocumentType)
46 | }
47 |
48 | const HK = {
49 | getCaseByCitation,
50 | getCaseByName,
51 | getPDF,
52 | }
53 |
54 | export default HK
--------------------------------------------------------------------------------
/src/utils/scraper/HK/HKLIIHK.ts:
--------------------------------------------------------------------------------
1 | import Request from '../../Request'
2 | import Constants from '../../Constants'
3 |
4 | const DOMAIN = `https://www.hklii.hk`
5 |
6 | const getCaseByCitation = async (citation: string): Promise => {
7 | const { data: searchData } = await Request.post(
8 | `${DOMAIN}/searchapi/nonlegis/_search/template?filter_path=hits.hits._source`,
9 | {
10 | id: `quickcitation`,
11 | params: {
12 | citation,
13 | },
14 | },
15 | )
16 |
17 | if(!searchData?.hits?.hits || !Array.isArray(searchData.hits.hits) || searchData.hits.hits.length !== 1){
18 | return []
19 | }
20 |
21 | const path = searchData.hits.hits[0]._source.path
22 | const { data } = await Request.post(
23 | `${DOMAIN}/searchapi/nonlegis/_search/`,
24 | {
25 | query: {
26 | term: {
27 | path,
28 | },
29 | },
30 | size: 1,
31 | },
32 | )
33 |
34 | if(!data?.hits?.hits || !Array.isArray(data.hits.hits) || data.hits.hits.length !== 1){
35 | return []
36 | }
37 |
38 | const {
39 | hits: {
40 | hits: [
41 | {
42 | _source: {
43 | title: name,
44 | neutral,
45 | path: casePath,
46 | },
47 | },
48 | ],
49 | },
50 | } = data
51 |
52 | return [{
53 | citation: neutral,
54 | database: Constants.DATABASES.HK_hklii,
55 | jurisdiction: Constants.JURISDICTIONS.HK.id,
56 | links: [
57 | {
58 | doctype: `Judgment`,
59 | filetype: `HTML`,
60 | url: `${DOMAIN}${casePath}`,
61 | },
62 | ],
63 | name,
64 | }]
65 | }
66 |
67 | const HKLII = {
68 | getCaseByCitation,
69 | }
70 |
71 | export default HKLII
--------------------------------------------------------------------------------
/src/utils/scraper/HK/HKLIIORG.ts:
--------------------------------------------------------------------------------
1 | import * as cheerio from 'cheerio'
2 | import Request from '../../Request'
3 | import Constants from '../../Constants'
4 | import Helpers from '../../Helpers'
5 | import { findHKCaseCitation } from '../../Finder/CaseCitationFinder/HK'
6 | import type { AxiosResponse } from 'axios'
7 | import PDF from '../../PDF'
8 | import { findCitation } from '../utils'
9 | import { CacheRequestConfig } from 'axios-cache-interceptor'
10 |
11 | const DOMAIN = `https://www.hklii.org`
12 |
13 | const parseCaseData = (data: AxiosResponse[`data`]): Law.Case[] => {
14 | const $ = cheerio.load(data)
15 |
16 | return $(`body ol[start="1"] > li`).map((_, element): Law.Case => {
17 | const nameText = $(`a:first-of-type`, element).eq(0).text().trim()
18 | const name = nameText.split(`[`)[0]
19 | const link = `${DOMAIN}${$(`a:nth-of-type(2)`, element).attr(`href`)}`
20 | const citation = findCitation(
21 | findHKCaseCitation,
22 | nameText,
23 | )
24 | return {
25 | citation,
26 | database: Constants.DATABASES.HK_hkliiorg,
27 | jurisdiction: Constants.JURISDICTIONS.HK.id,
28 | links: [
29 | {
30 | doctype: `Judgment`,
31 | filetype: `HTML`,
32 | url: link,
33 | },
34 | ],
35 | name,
36 | }
37 | }).get()
38 | .filter(({ citation}) => Helpers.isCitationValid(citation))
39 | }
40 |
41 | const getCaseByCitation = async (citation: string): Promise => {
42 | const { data } = await Request.get(
43 | `${DOMAIN}/cgi-bin/sinosrchadvanced.cgi`,
44 | {
45 | params: {
46 | citation,
47 | meta: `hklii`,
48 | method: `boolean`,
49 | mode: `advanced`,
50 | results: 20,
51 | },
52 | } as CacheRequestConfig,
53 | )
54 |
55 | return parseCaseData(data)
56 | }
57 |
58 | const getCaseByName = async (caseName: string): Promise => {
59 | const { data } = await Request.get(
60 | `${DOMAIN}/cgi-bin/sinosrchadvanced.cgi`,
61 | {
62 | params: {
63 | meta: `hklii`,
64 | method: `boolean`,
65 | mode: `advanced`,
66 | results: 20,
67 | titleall: caseName,
68 | },
69 | } as CacheRequestConfig,
70 | )
71 |
72 | return parseCaseData(data)
73 | }
74 |
75 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
76 | const judgmentLink = Helpers.getJudgmentLink(inputCase.links)?.url
77 | const fileName = Helpers.getFileName(inputCase, inputDocumentType)
78 | await PDF.save({
79 | code: `document.body.innerHTML = document.querySelector('#main-content').innerHTML;`
80 | + `const immediateChildren = document.querySelectorAll('body> *');`
81 | + `const hrList = [];`
82 | + `immediateChildren.forEach((el, i) => {if(el.nodeName === 'HR') {hrList.push(i)}});`
83 | + `immediateChildren.forEach((el, i) => {if(i <= hrList[0]){el.remove()}});`
84 | + `document.querySelector('body').setAttribute('style', 'font-family: Times New Roman;');`
85 | + `document.querySelectorAll('a').forEach((el) => el.style = 'color: black !important; text-decoration: none;');`,
86 | fileName,
87 | url: judgmentLink,
88 | })
89 | return null
90 | }
91 |
92 | const HKLIIORG = {
93 | getCaseByCitation,
94 | getCaseByName,
95 | getPDF,
96 | }
97 |
98 | export default HKLIIORG
--------------------------------------------------------------------------------
/src/utils/scraper/HK/__tests__/LRS.test.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import Request from '../../../Request'
4 | import { getCaseByCitation } from '../LRS'
5 |
6 | jest.mock(`../../../Request`)
7 | const RequestGet = Request.get as jest.Mock
8 |
9 | const citationQuery = `[2021] HKCFA 14`
10 |
11 | describe(`HK LRS`, () => {
12 | it(`should send valid request for getCaseByCitation`, () => {
13 | RequestGet.mockImplementation(() => Promise.resolve({ data: `` }))
14 |
15 | getCaseByCitation(citationQuery)
16 | expect(RequestGet.mock.calls).toMatchSnapshot()
17 | })
18 |
19 | it(`should parse getCaseByCitation result correctly`, async () => {
20 | expect.assertions(1)
21 | const mockSearchResponse = fs.readFileSync(
22 | path.join(__dirname, `responses`, `LRS`, `getCaseByCitation-search.txt`),
23 | `utf-8`,
24 | )
25 | const mockResultResponse = fs.readFileSync(
26 | path.join(__dirname, `responses`, `LRS`, `getCaseByCitation-result.txt`),
27 | `utf-8`,
28 | )
29 | RequestGet
30 | .mockImplementationOnce(() => Promise.resolve({ data: mockSearchResponse }))
31 | .mockImplementationOnce(() => Promise.resolve({ data: mockResultResponse }))
32 |
33 | const result = await getCaseByCitation(citationQuery)
34 | expect(result).toMatchSnapshot()
35 | })
36 | })
--------------------------------------------------------------------------------
/src/utils/scraper/HK/__tests__/__snapshots__/LRS.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`HK LRS should parse getCaseByCitation result correctly 1`] = `
4 | [
5 | {
6 | "citation": "[2021] HKCFA 14",
7 | "database": {
8 | "icon": "",
9 | "id": "HK_lrs",
10 | "name": "Legal Reference System",
11 | "url": "https://www.judiciary.hk/en/legal_ref/judgments.htm",
12 | },
13 | "jurisdiction": "HK",
14 | "links": [
15 | {
16 | "doctype": "Judgment",
17 | "filetype": "HTML",
18 | "url": "https://legalref.judiciary.hk/lrs/common/ju/ju_frame.jsp?DIS=135729&currpage=T",
19 | },
20 | ],
21 | "name": "FACV11/2020 HSIN CHONG CONSTRUCTION COMPANY LIMITED (in liquidation) v. BUILD KING CONSTRUCTION LIMITED",
22 | },
23 | ]
24 | `;
25 |
26 | exports[`HK LRS should send valid request for getCaseByCitation 1`] = `
27 | [
28 | [
29 | "https://legalref.judiciary.hk/lrs/common/search/jud_search_ncn.jsp",
30 | {
31 | "params": {
32 | "isadvsearch": "1",
33 | "ncnLanguage": "en",
34 | "ncnParagraph": "",
35 | "ncnValue": "[2021] HKCFA 14",
36 | "query": "Go!",
37 | "selDatabase": "JU",
38 | "selall": "0",
39 | "txtSearch": "[2021] HKCFA 14",
40 | "txtselectopt": "4",
41 | },
42 | },
43 | ],
44 | ]
45 | `;
46 |
--------------------------------------------------------------------------------
/src/utils/scraper/HK/__tests__/responses/LRS/getCaseByCitation-search.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Legal Reference System
6 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/utils/scraper/HK/index.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export {default} from './HK'
--------------------------------------------------------------------------------
/src/utils/scraper/MY/Kehakiman.ts:
--------------------------------------------------------------------------------
1 | import * as cheerio from 'cheerio'
2 | import Request from '../../Request'
3 | import Constants from '../../Constants'
4 | import { AxiosResponse } from 'axios'
5 | import { CacheRequestConfig } from 'axios-cache-interceptor'
6 |
7 | const DOMAIN = `https://ejudgment.kehakiman.gov.my`
8 |
9 | const parseCases = (data: AxiosResponse[`data`]): Law.Case[] => {
10 | const $ = cheerio.load(data)
11 | return $(`table#CUSTOM_Ap > tbody > tr`).map((_, element) => {
12 | const citation = $(`td:nth-of-type(2)`, element).text().trim()
13 | const parties = $(`td:nth-of-type(3)`, element).text().trim()
14 | const pdfURL = $(`td:nth-of-type(7) a`, element).attr(`href`)
15 | const pdfLink: Law.Link = {
16 | doctype: `Judgment`,
17 | filetype: `PDF`,
18 | url: (new RegExp(`^//ejudgment.kehakiman.gov.my`)).test(pdfURL)
19 | ? `https:${pdfURL}`
20 | : `${DOMAIN}${pdfURL}`,
21 | }
22 | return {
23 | citation,
24 | database: Constants.DATABASES.MY_kehakiman,
25 | jurisdiction: Constants.JURISDICTIONS.MY.id,
26 | links: [
27 | ...(pdfURL ? [pdfLink] : []),
28 | ],
29 | name: parties,
30 | }
31 | }).get()
32 | }
33 |
34 | const getCaseByName = async (caseName: string): Promise => {
35 | const { data } = await Request.get(
36 | `${DOMAIN}/portal/ap_list_all.php`,
37 | {
38 | params: {
39 | aph_ap_date: ``,
40 | aph_case_no: caseName,
41 | aph_case_type: ``,
42 | aph_court_category: ``,
43 | nama_hakim: ``,
44 | },
45 | } as CacheRequestConfig,
46 | )
47 | return parseCases(data)
48 | }
49 |
50 | const getCaseByCitation = async (citation: string): Promise => {
51 | const { data } = await Request.get(
52 | `${DOMAIN}/portal/ap_list_all.php`,
53 | {
54 | params: {
55 | aph_ap_date: ``,
56 | aph_case_no: citation,
57 | aph_case_type: ``,
58 | aph_court_category: ``,
59 | nama_hakim: ``,
60 | },
61 | } as CacheRequestConfig,
62 | )
63 | return parseCases(data)
64 | }
65 |
66 | const Kehakiman = {
67 | getCaseByCitation,
68 | getCaseByName,
69 | }
70 |
71 | export default Kehakiman
--------------------------------------------------------------------------------
/src/utils/scraper/MY/MY.ts:
--------------------------------------------------------------------------------
1 | import Kehakiman from './Kehakiman'
2 | import Common from '../common'
3 | import Constants from '../../Constants'
4 | import { sortMYCases } from '../../Finder/CaseCitationFinder/MY'
5 | import { databaseUseDatabase, databaseUseJurisdiction, makeEventTarget } from '../utils'
6 |
7 | const databaseUseMY = databaseUseJurisdiction(`MY`)
8 | const databaseUseCommonLII = databaseUseDatabase(`commonlii`, databaseUseMY)
9 | const databaseUseKehakiman = databaseUseDatabase(`kehakiman`, databaseUseMY)
10 |
11 | const getCaseByName = (caseName: string): EventTarget => makeEventTarget(
12 | caseName,
13 | [
14 | databaseUseCommonLII(() => Common.CommonLII.getCaseByName(caseName, Constants.JURISDICTIONS.MY.name)),
15 | databaseUseKehakiman(() => Kehakiman.getCaseByName(caseName)),
16 | ],
17 | `MY`,
18 | sortMYCases,
19 | true,
20 | )
21 |
22 | const getCaseByCitation = (citation: string): EventTarget => makeEventTarget(
23 | citation,
24 | [
25 | databaseUseCommonLII(() => Common.CommonLII.getCaseByCitation(citation)),
26 | databaseUseKehakiman(() => Kehakiman.getCaseByCitation(citation)),
27 | ],
28 | `MY`,
29 | sortMYCases,
30 | false,
31 | )
32 |
33 | const databaseMap = {
34 | [Constants.DATABASES.commonlii.id]: Common.CommonLII,
35 | }
36 |
37 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
38 | const { database } = inputCase
39 | return databaseMap[database.id].getPDF(inputCase, inputDocumentType)
40 | }
41 |
42 | const MY = {
43 | getCaseByCitation,
44 | getCaseByName,
45 | getPDF,
46 | }
47 |
48 | export default MY
--------------------------------------------------------------------------------
/src/utils/scraper/MY/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './MY'
--------------------------------------------------------------------------------
/src/utils/scraper/NZ/NZ.ts:
--------------------------------------------------------------------------------
1 | import nzlii from './nzlii'
2 | import Common from '../common'
3 | import Constants from '../../Constants'
4 | import { sortNZCases } from '../../Finder/CaseCitationFinder/NZ'
5 | import { databaseUseDatabase, databaseUseJurisdiction, makeEventTarget } from '../utils'
6 |
7 | const databaseUseNZ = databaseUseJurisdiction(`NZ`)
8 | const databaseUseNZLII = databaseUseDatabase(`nzlii`, databaseUseNZ)
9 | const databaseUseCommonLII = databaseUseDatabase(`commonlii`, databaseUseNZ)
10 |
11 | const getCaseByName = (caseName: string): EventTarget => makeEventTarget(
12 | caseName,
13 | [
14 | databaseUseNZLII(() => nzlii.getCaseByName(caseName)),
15 | databaseUseCommonLII(() => Common.CommonLII.getCaseByName(caseName, Constants.JURISDICTIONS.NZ.name)),
16 | ],
17 | `NZ`,
18 | sortNZCases,
19 | true,
20 | )
21 |
22 | const getCaseByCitation = (citation: string): EventTarget => makeEventTarget(
23 | citation,
24 | [
25 | databaseUseNZLII(() => nzlii.getCaseByCitation(citation)),
26 | databaseUseCommonLII(() => Common.CommonLII.getCaseByCitation(citation)),
27 | ],
28 | `NZ`,
29 | sortNZCases,
30 | false,
31 | )
32 |
33 | const databaseMap = {
34 | [Constants.DATABASES.NZ_nzlii.id]: nzlii,
35 | [Constants.DATABASES.commonlii.id]: Common.CommonLII,
36 | }
37 |
38 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
39 | const { database } = inputCase
40 | return databaseMap[database.id].getPDF(inputCase, inputDocumentType)
41 | }
42 |
43 | const NZ = {
44 | getCaseByCitation,
45 | getCaseByName,
46 | getPDF,
47 | }
48 |
49 | export default NZ
--------------------------------------------------------------------------------
/src/utils/scraper/NZ/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './NZ'
--------------------------------------------------------------------------------
/src/utils/scraper/SG/SGSC.ts:
--------------------------------------------------------------------------------
1 | import * as cheerio from 'cheerio'
2 | import Request from '../../Request'
3 | import Constants from '../../Constants'
4 | import Logger from '../../Logger'
5 |
6 | const DOMAIN = `https://www.supremecourt.gov.sg`
7 | const getSearchResults = (citation: string) => `${DOMAIN}/search-judgment?q=${citation}&y=All`
8 |
9 | const parseCase = ($: cheerio.CheerioAPI, cheerioElement: cheerio.Element): Law.Case => {
10 | const name = ($(`.text`, cheerioElement).contents().get(2) as unknown as cheerio.CheerioAPI).text().trim()
11 | const link = $(`.doc-download`, cheerioElement).attr(`href`)
12 | const pdf = `${DOMAIN}${$(`.pdf-download`, cheerioElement).attr(`href`)}`
13 | const citation = $(`.text ul.decision li`, cheerioElement).eq(0).text().trim()
14 |
15 | const summaryLink: Law.Link | null = link
16 | ? { doctype: `Summary`, filetype: `HTML`, url: `${DOMAIN}${link}` }
17 | : null
18 | const pdfLink:Law.Link = { doctype: `Judgment`, filetype: `PDF`, url: pdf }
19 | return {
20 | citation,
21 | database: Constants.DATABASES.SG_sc,
22 | jurisdiction: Constants.JURISDICTIONS.SG.id,
23 | links: [
24 | ...(summaryLink ? [summaryLink] : []),
25 | pdfLink,
26 | ],
27 | name,
28 | }
29 | }
30 |
31 | const trimLeadingPageZeros = (citation: string) => citation.replace(/ 0+([1-9]+)$/, ` $1`)
32 |
33 | const getCaseByCitation = async (citation: string): Promise => {
34 | const { data } = await Request.get(getSearchResults(citation))
35 | const $ = cheerio.load(data)
36 |
37 | const results = $(`.judgmentpage`)
38 | .map((_, element) => parseCase($, element))
39 | .get()
40 | .filter(({ citation: scrapedCitation })=> (
41 | trimLeadingPageZeros(scrapedCitation).toLowerCase() === citation.toLowerCase()
42 | ))
43 | Logger.log(`SGSC scrape results`, results)
44 | return results
45 | }
46 |
47 | const getCaseByName = async (caseName: string): Promise => {
48 | const { data } = await Request.get(getSearchResults(caseName))
49 | const $ = cheerio.load(data)
50 |
51 | const results = $(`.judgmentpage`).map((_, element) => parseCase($, element)).get()
52 | Logger.log(`SGSC scrape results`, results)
53 | return results
54 | }
55 |
56 | const SGSC = {
57 | getCaseByCitation,
58 | getCaseByName,
59 | getSearchResults,
60 | }
61 |
62 | export default SGSC
--------------------------------------------------------------------------------
/src/utils/scraper/SG/SLW.ts:
--------------------------------------------------------------------------------
1 | import Request from '../../Request'
2 | import Constants from '../../Constants'
3 | import { findSGCaseCitation } from '../../Finder/CaseCitationFinder/SG'
4 | import Helpers from '../../Helpers'
5 | import Logger from '../../Logger'
6 |
7 | const DOMAIN = `https://www.singaporelawwatch.sg`
8 |
9 | const getSearchResults = (citation: string) => `${DOMAIN}/DesktopModules/EasyDNNNewsSearch/SearchAutoComplete.ashx?nsw=a&mid=479&TabId=21&portal_id=0&acat=1&ModToOpenResults=426&TabToOpenResults=48&evl=0&q=${citation}`
10 |
11 | const parseCase = (name: string, link: string) => ({
12 | citation: findSGCaseCitation(name)[0]?.citation,
13 | database: Constants.DATABASES.SG_slw,
14 | jurisdiction: Constants.JURISDICTIONS.SG.id,
15 | links: [
16 | {
17 | doctype: `Judgment`,
18 | filetype: `PDF`,
19 | url: link,
20 | },
21 | ],
22 | name: name.split(`[`)[0].trim(),
23 | })
24 |
25 | const getCaseByCitation = async (citation: string): Promise => {
26 | const { data } = await Request.get(getSearchResults(citation))
27 | const results = data
28 | .map(([name, link]) => parseCase(name, link))
29 | .filter(({ citation: scrapedCitation }) => scrapedCitation && citation.toLowerCase() === scrapedCitation.toLowerCase())
30 | Logger.log(`SLW scrape results`, results)
31 | return results
32 | }
33 |
34 | const getCaseByName = async (caseName: string): Promise => {
35 | const { data } = await Request.get(getSearchResults(caseName))
36 |
37 | const results = data
38 | .map(([name, link]) => parseCase(name, link))
39 | .filter(({ citation}) => Helpers.isCitationValid(citation))
40 | Logger.log(`SLW scrape results`, results)
41 | return results
42 | }
43 |
44 | const SLW = {
45 | getCaseByCitation,
46 | getCaseByName,
47 | getSearchResults,
48 | }
49 |
50 | export default SLW
--------------------------------------------------------------------------------
/src/utils/scraper/SG/__tests__/OpenLaw.test.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import Request from 'utils/Request'
4 | import OpenLaw from '../OpenLaw'
5 |
6 | jest.mock(`utils/Request`)
7 | const RequestPost = Request.post as jest.Mock
8 |
9 | const CITATION = `[1999] SGHC 283`
10 |
11 | describe(`SG OpenLaw`, () => {
12 | it(`should send valid request for getCaseByCitation`, () => {
13 | RequestPost.mockImplementationOnce(() => Promise.resolve({
14 | data: ``,
15 | }))
16 | OpenLaw.getCaseByCitation(CITATION)
17 | expect(RequestPost.mock.calls).toMatchSnapshot()
18 | })
19 | it(`should parse getCaseByCitation result correctly`, async () => {
20 | expect.assertions(1)
21 | const mockResponse = JSON.parse(fs.readFileSync(
22 | path.join(__dirname, `responses`, `OpenLaw`, `getCaseByCitation-result.txt`),
23 | ).toString())
24 | RequestPost.mockImplementationOnce(() => Promise.resolve({ data: mockResponse }))
25 | const result = await OpenLaw.getCaseByCitation(CITATION)
26 | expect(result).toMatchSnapshot()
27 | })
28 | })
--------------------------------------------------------------------------------
/src/utils/scraper/SG/__tests__/STB.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | SGSTBIsLongFormat,
3 | SGSTBIsSquareBracketFormat,
4 | } from '../STB'
5 |
6 | describe(`SG STB`, () => {
7 | const LONG_CITATIONS = [
8 | `STB 31 of 2021`,
9 | `STB 117 of 2018`,
10 | `STB No.65 of 2016`,
11 | `STB 77 and 95 of 2012`,
12 | `STB 59A of 2011`,
13 | `STB No 52/64/65/66 of 2011`,
14 | ]
15 | const SQUARE_BRACKET_CITATIONS = [
16 | `[2021] SGSTB 31`,
17 | `[2018] SGSTB 117`,
18 | `[2016] SGSTB 65`,
19 | `[2012] SGSTB 77`,
20 | `[2011] SGSTB 59A`,
21 | `[2011] SGSTB 52`,
22 | ]
23 |
24 | test.concurrent.each(
25 | LONG_CITATIONS,
26 | )(`should correctly identify %s as a long citation`, (citation) => {
27 | expect(SGSTBIsLongFormat(citation)).toBe(true)
28 | })
29 |
30 | test.concurrent.each(
31 | SQUARE_BRACKET_CITATIONS,
32 | )(`should correctly identify %s as not a long citation`, (citation) => {
33 | expect(SGSTBIsLongFormat(citation)).toBe(false)
34 | })
35 |
36 | test.concurrent.each(
37 | SQUARE_BRACKET_CITATIONS,
38 | )(`should correctly identify %s as a square bracket citation`, (citation) => {
39 | expect(SGSTBIsSquareBracketFormat(citation)).toBe(true)
40 | })
41 |
42 | // test.concurrent.each(
43 | // LONG_CITATIONS,
44 | // )(`should correctly identify %s as not a square bracket citation`, (citation) => {
45 | // expect(SGSTBIsSquareBracketFormat(citation)).toBe(false)
46 | // })
47 |
48 | // test.concurrent.each(
49 | // LONG_CITATIONS.map((c, index) => ([c, SQUARE_BRACKET_CITATIONS[index]])),
50 | // )(`should correctly convert %s to %s`, (longCitation, squareBracketCitation) => {
51 | // expect(SGSTBSquareBracketFormat(longCitation)).toBe(squareBracketCitation)
52 | // })
53 | })
--------------------------------------------------------------------------------
/src/utils/scraper/SG/__tests__/__snapshots__/OpenLaw.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SG OpenLaw should parse getCaseByCitation result correctly 1`] = `[]`;
4 |
5 | exports[`SG OpenLaw should send valid request for getCaseByCitation 1`] = `
6 | [
7 | [
8 | "https://api.lawnet.sg/lawnet/search-service/api/lawnetcore/search/supreme-court",
9 | {
10 | "data": {
11 | "filters": null,
12 | "includeDraft": "true",
13 | "orderBy": "relevancy-desc",
14 | "pageLength": 10,
15 | "searchDatabase": "allOfLawnet",
16 | "searchQuery": ""[1999] SGHC 283"",
17 | "searchSource": null,
18 | "start": 1,
19 | },
20 | "info": {
21 | "appId": "APP_LAWNET_ONE",
22 | "sessionId": null,
23 | "userDevice": {
24 | "isBot": false,
25 | },
26 | "userId": 0,
27 | "userName": null,
28 | },
29 | },
30 | {
31 | "headers": {
32 | "Auth-Type": "Public",
33 | },
34 | },
35 | ],
36 | ]
37 | `;
38 |
--------------------------------------------------------------------------------
/src/utils/scraper/SG/eLitigation.ts:
--------------------------------------------------------------------------------
1 | import { load } from 'cheerio'
2 | import Request from '../../Request'
3 | import Constants from '../../Constants'
4 | import Logger from '../../Logger'
5 | import { CacheRequestConfig } from 'axios-cache-interceptor'
6 |
7 | const DOMAIN = `https://www.elitigation.sg`
8 |
9 | const parseCaseResults = (data: string): Law.Case[] => {
10 | const $ = load(data)
11 | return $(`#listview > .row > .card.col-12`).map((_, element) => {
12 | const card = $(`> .card-body > .row`, element)
13 | const name = $(`a.gd-card-title,a.gd-heardertext`, card).text().trim()
14 | const link = $(`a.gd-card-title,a.gd-heardertext`, card).attr(`href`)
15 | const pdfPath = link.replace(`SUPCT/`, ``).replace(`/s/`, `/gd/`)
16 | const pdf = $(`img.card-icon`, card).parent().attr(`href`) || `${pdfPath}/pdf`
17 | const citation = $(`span.gd-addinfo-text`, card).first().text().trim().replace(`|`, ``).trim()
18 |
19 | const summaryLink = link
20 | ? { doctype: `Summary`, filetype: `HTML`, url: `${DOMAIN}${link}` } as Law.Link
21 | : null
22 |
23 | const pdfLink = pdf
24 | ? { doctype: `Judgment`, filetype: `PDF`, url: `${DOMAIN}${pdf}` } as Law.Link
25 | : null
26 |
27 | return {
28 | citation,
29 | database: Constants.DATABASES.SG_elitigation,
30 | jurisdiction: Constants.JURISDICTIONS.SG.id,
31 | links: [
32 | ...(summaryLink ? [summaryLink] : []),
33 | ...(pdfLink ? [pdfLink] : []),
34 | ],
35 | name,
36 | }
37 |
38 | }).get()
39 | }
40 |
41 | const trimLeadingPageZeros = (citation: string) => citation.replace(/ 0+([1-9]+)$/, ` $1`)
42 |
43 | const getCaseByCitation = async (citation: string): Promise => {
44 | // const abbr = citation.match(makeCaseCitationRegex(SGSCAbbrs))
45 | // Logger.log(`abbr`, abbr)
46 |
47 | const { data } = await Request.get(`${DOMAIN}/gdviewer/Home/Index`,
48 | {
49 | params: {
50 | currentPage: `1`,
51 | searchPhrase: `"${citation}"`,
52 | sortAscending: `False`,
53 | sortBy: `Score`,
54 | verbose: `False`,
55 | yearOfDecision: `All`,
56 | },
57 | } as CacheRequestConfig)
58 |
59 | return parseCaseResults(data).filter(({ citation: scrapedCitation }) => (
60 | trimLeadingPageZeros(scrapedCitation).toLowerCase() === citation.toLowerCase()
61 | ))
62 | }
63 |
64 | const getCaseByName = async (caseName: string): Promise => {
65 | try {
66 | const { data } = await Request.get(`${DOMAIN}/gdviewer/Home/Index`,
67 | {
68 | params: {
69 | currentPage: `1`,
70 | searchPhrase: caseName,
71 | sortAscending: `False`,
72 | sortBy: `Score`,
73 | verbose: `False`,
74 | yearOfDecision: `All`,
75 | },
76 | } as CacheRequestConfig)
77 | return parseCaseResults(data)
78 | } catch (error){
79 | Logger.error(error)
80 | throw error
81 | }
82 | }
83 |
84 | const eLitigation = {
85 | getCaseByCitation,
86 | getCaseByName,
87 | }
88 |
89 | export default eLitigation
--------------------------------------------------------------------------------
/src/utils/scraper/SG/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './SG'
--------------------------------------------------------------------------------
/src/utils/scraper/Scraper.ts:
--------------------------------------------------------------------------------
1 | import Memoize from 'memoizee'
2 | import SG from './SG'
3 | import UK from './UK'
4 | import EU from './EU'
5 | import HK from './HK'
6 | import CA from './CA'
7 | import AU from './AU'
8 | import NZ from './NZ'
9 | import MY from './MY'
10 | import ECHR from './ECHR'
11 | import Constants from '../Constants'
12 | import Logger from '../Logger'
13 | import UN from './UN'
14 |
15 | const jurisdictionMap = {
16 | [Constants.JURISDICTIONS.AU.id]: AU,
17 | [Constants.JURISDICTIONS.EU.id]: EU,
18 | [Constants.JURISDICTIONS.CA.id]: CA,
19 | [Constants.JURISDICTIONS.HK.id]: HK,
20 | [Constants.JURISDICTIONS.NZ.id]: NZ,
21 | [Constants.JURISDICTIONS.SG.id]: SG,
22 | [Constants.JURISDICTIONS.UK.id]: UK,
23 | [Constants.JURISDICTIONS.MY.id]: MY,
24 | [Constants.JURISDICTIONS.ECHR.id]: ECHR,
25 | [Constants.JURISDICTIONS.UN.id]: UN,
26 | }
27 |
28 | const getCaseByCitation = (
29 | targetCase: Finder.CaseCitationFinderResult,
30 | inputJurisdiction: Law.JurisdictionCode = null,
31 | ): EventTarget => {
32 | const { jurisdiction, citation, court } = targetCase
33 |
34 | const targetJurisdiction = inputJurisdiction === null
35 | ? jurisdictionMap[jurisdiction]
36 | : jurisdictionMap[inputJurisdiction]
37 |
38 | return targetJurisdiction.getCaseByCitation(
39 | citation,
40 | court,
41 | )
42 | }
43 |
44 | const getCaseByName = (
45 | targetCaseName: Finder.CaseNameFinderResult,
46 | inputJurisdiction: Law.JurisdictionCode,
47 | ) : EventTarget => {
48 | const { name } = targetCaseName
49 | const targetJurisdiction = jurisdictionMap[inputJurisdiction]
50 |
51 | if(!targetJurisdiction || !targetJurisdiction?.getCaseByName){
52 | Logger.error(`targetJurisdiction or getCaseByName missing`)
53 | }
54 |
55 | return targetJurisdiction.getCaseByName(name)
56 | }
57 |
58 | const getPDF = Memoize((
59 | inputCase: Law.Case,
60 | inputDocumentType: Law.Link[`doctype`],
61 | ): Promise => {
62 | const existingLink = inputCase.links.find(({ doctype, filetype }) => doctype === inputDocumentType && filetype === `PDF`)
63 | if(existingLink){
64 | return Promise.resolve(existingLink?.url)
65 | }
66 |
67 | const targetJurisdiction = jurisdictionMap[inputCase.jurisdiction]
68 | if(!targetJurisdiction || !(`getPDF` in targetJurisdiction)){
69 | return Promise.resolve(``)
70 | }
71 |
72 | return (
73 | targetJurisdiction as any
74 | ).getPDF(inputCase, inputDocumentType)
75 | }, {
76 | normalizer: ([inputCase, inputDocumentType]) => {
77 | const { citation, jurisdiction } = inputCase
78 | return `${jurisdiction}-${citation}-${inputDocumentType}`
79 | },
80 | })
81 |
82 | const scraper = {
83 | AU,
84 | CA,
85 | ECHR,
86 | EU,
87 | HK,
88 | MY,
89 | NZ,
90 | SG,
91 | UK,
92 | getCaseByCitation,
93 | getCaseByName,
94 | getPDF,
95 | }
96 |
97 | export default scraper
--------------------------------------------------------------------------------
/src/utils/scraper/UK/FindCaseLaw.ts:
--------------------------------------------------------------------------------
1 | import * as cheerio from 'cheerio'
2 | import Request from '../../Request'
3 | import Constants from 'utils/Constants'
4 | import Helpers from 'utils/Helpers'
5 |
6 | const ASSETS_DOMAIN = `https://assets.caselaw.nationalarchives.gov.uk`
7 | const DOMAIN = `https://caselaw.nationalarchives.gov.uk`
8 |
9 | const generatePDFPath = (path: string): string => {
10 | const segments = path.slice(1).split(`/`).join(`_`)
11 | return `${path}/${segments}.pdf`
12 | }
13 |
14 | const parseCases = (html: string): Law.Case[] => {
15 | const $ = cheerio.load(html)
16 | if($(`.results__results-intro`).text().trim() === `We found 0 judgments`){
17 | return []
18 | }
19 | return $(`.results__result-list-container > .judgment-listing__list > li`).map((_, element): Law.Case => {
20 | const name = $(`span.judgment-listing__title`, element).text().trim()
21 | const citation = $(`span.judgment-listing__neutralcitation`, element).text().trim()
22 | const path = $(`span.judgment-listing__title> a`, element).attr(`href`)
23 | const link = DOMAIN + path
24 |
25 | return {
26 | citation,
27 | database: Constants.DATABASES.UK_findcaselaw,
28 | jurisdiction: Constants.JURISDICTIONS.UK.id,
29 | links: [
30 | {
31 | doctype: `Judgment`,
32 | filetype: `HTML`,
33 | url: link,
34 | },
35 | {
36 | doctype: `Judgment`,
37 | filetype: `PDF`,
38 | url: ASSETS_DOMAIN + generatePDFPath(path),
39 | },
40 | ],
41 | name,
42 | }
43 | }).get().filter(({ citation }) => Helpers.isCitationValid(citation))
44 | }
45 |
46 | const getCaseByCitation = async (citation: string): Promise => {
47 | const response = await Request.get(
48 | `${DOMAIN}/judgments/advanced_search`,
49 | {
50 | params: {
51 | from: ``,
52 | judge: ``,
53 | order: `-relevance`,
54 | party: ``,
55 | per_page: 10,
56 | query: `"${citation}"`,
57 | to: ``,
58 | },
59 | },
60 | )
61 | return parseCases(response.data)
62 | }
63 |
64 | const getCaseByName = async (caseName: string): Promise => {
65 | const response = await Request.get(
66 | `${DOMAIN}/judgments/advanced_search`,
67 | {
68 | params: {
69 | from: ``,
70 | judge: ``,
71 | order: `-relevance`,
72 | party: ``,
73 | per_page: 10,
74 | query: caseName,
75 | to: ``,
76 | },
77 | },
78 | )
79 | return parseCases(response.data)
80 | }
81 |
82 | const FindCaseLaw = {
83 | getCaseByCitation,
84 | getCaseByName,
85 | }
86 |
87 | export default FindCaseLaw
--------------------------------------------------------------------------------
/src/utils/scraper/UK/UK.ts:
--------------------------------------------------------------------------------
1 | import BAILII from './BAILII'
2 | import Common from '../common'
3 | import Custom from '../custom'
4 | import { sortUKCases } from '../../Finder/CaseCitationFinder/UK'
5 | import Constants from '../../Constants'
6 | import { databaseUseDatabase, databaseUseJurisdiction, makeEventTarget } from '../utils'
7 | import UKIPO from './UKIPO'
8 | import FindCaseLaw from './FindCaseLaw'
9 |
10 | const databaseUseUK = databaseUseJurisdiction(`UK`)
11 | const databaseUseBailii = databaseUseDatabase(`bailii`, databaseUseUK)
12 | const databaseUseCommonLII = databaseUseDatabase(`commonlii`, databaseUseUK)
13 | const databaseUseIPO = databaseUseDatabase(`ipo`, databaseUseUK)
14 | const databaseUseFindCaseLaw = databaseUseDatabase(`findcaselaw`, databaseUseUK)
15 |
16 | const getCaseByName = (caseName: string): EventTarget => makeEventTarget(
17 | caseName,
18 | [
19 | databaseUseFindCaseLaw(() => FindCaseLaw.getCaseByName(caseName)),
20 | databaseUseBailii(() => BAILII.getCaseByName(caseName)),
21 | databaseUseCommonLII(() => Common.CommonLII.getCaseByName(caseName, Constants.JURISDICTIONS.UK.name)),
22 | databaseUseIPO(() => UKIPO.getCaseByName(caseName)),
23 | Custom.getCaseByName(caseName),
24 | ],
25 | `UK`,
26 | sortUKCases,
27 | true,
28 | )
29 |
30 | const getCaseByCitation = (citation: string, court: string): EventTarget => makeEventTarget(
31 | citation,
32 | court === `UKIPO` ? [
33 | databaseUseIPO(() => UKIPO.getCaseByCitation(citation)),
34 | ] : [
35 | Custom.getCaseByCitation(citation, court),
36 | databaseUseFindCaseLaw(() => FindCaseLaw.getCaseByCitation(citation)),
37 | databaseUseBailii(() => BAILII.getCaseByCitation(citation)),
38 | databaseUseCommonLII(() => Common.CommonLII.getCaseByCitation(citation)),
39 | ],
40 | `UK`,
41 | sortUKCases,
42 | false,
43 | )
44 |
45 | const databaseMap = {
46 | [Constants.DATABASES.UK_bailii.id]: BAILII,
47 | [Constants.DATABASES.commonlii.id]: Common.CommonLII,
48 | [Constants.DATABASES.UK_ipo.id]: UKIPO,
49 | }
50 |
51 | const getPDF = async (inputCase: Law.Case, inputDocumentType: Law.Link[`doctype`]): Promise => {
52 | const { database } = inputCase
53 | return databaseMap[database.id].getPDF(inputCase, inputDocumentType)
54 | }
55 |
56 | const UK = {
57 | getCaseByCitation,
58 | getCaseByName,
59 | getPDF,
60 | }
61 |
62 | export default UK
--------------------------------------------------------------------------------
/src/utils/scraper/UK/Westlaw.ts:
--------------------------------------------------------------------------------
1 | // import axios from 'axios'
2 |
3 | const getCase = async (citation) => {}
4 |
5 | const Westlaw = {
6 | getCase,
7 | }
8 |
9 | export default Westlaw
--------------------------------------------------------------------------------
/src/utils/scraper/UK/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './UK'
--------------------------------------------------------------------------------
/src/utils/scraper/UN/ICJCIJ.ts:
--------------------------------------------------------------------------------
1 | import request from '../../Request'
2 | import * as cheerio from 'cheerio'
3 | import Constants from '../../Constants'
4 | import { sortByName } from '../utils'
5 | import Logger from 'utils/Logger'
6 |
7 | const BASE_URL = `https://www.icj-cij.org`
8 |
9 | const getDocumentTypeFromDocumentName = (docname: string): Law.Link[`doctype`] | null => {
10 | const cleanDocname = docname.toLowerCase()
11 | if(cleanDocname.includes(`order`)){
12 | return `Order`
13 | } else if (cleanDocname.includes(`judgment`)){
14 | return `Judgment`
15 | } else if (cleanDocname.includes(`opinion`)){
16 | return `Opinion`
17 | }
18 | return null
19 | }
20 |
21 | const getAllCases = async (): Promise => {
22 | const year = (new Date()).getFullYear()
23 | const { data } = await request.get(`${BASE_URL}/en/decisions?type=1&from=1946&to=${year}&sort_bef_combine=order_DESC`)
24 | const $ = cheerio.load(data)
25 | return $(`.view-judgments-advisory-opinions-and-orders > .view-content.row > .views-row`).map((_, element): Law.Case | null => {
26 | const name = $(`.views-field.views-field-field-case-long-title a`, element).text().trim().replace(`\n`, ``)
27 | const details = $(`.views-field.views-field-field-icj-document-subtitle p`, element).text().trim()
28 | const caseName = name + (details.length > 0 ? ` - ${details}` : ``)
29 | const docname = $(`.views-field.views-field-field-document-long-title a`, element).text().trim().replace(`\n`, ``)
30 | const doctype = getDocumentTypeFromDocumentName(docname)
31 | const summaryURL = BASE_URL + $(`.views-field.views-field-field-case-long-title a`, element).attr(`href`)
32 | const documentURL = BASE_URL + $(`.views-field.views-field-field-document-long-title a`, element).attr(`href`)
33 | if(doctype === null){
34 | return null
35 | }
36 | return {
37 | citation: docname,
38 | database: Constants.DATABASES.UN_icjcij,
39 | jurisdiction: Constants.JURISDICTIONS.UN.id,
40 | links: [
41 | {
42 | doctype: `Summary`,
43 | filetype: `HTML`,
44 | url: summaryURL,
45 | },
46 | {
47 | doctype,
48 | filetype: `PDF`,
49 | url: documentURL,
50 | },
51 | ],
52 | name: caseName,
53 | }
54 |
55 | }).get().filter((c) => c !== null)
56 | }
57 |
58 | const getCaseByName = async (caseName: string): Promise => {
59 | const cases = await getAllCases()
60 | Logger.log(`Cases: `, cases)
61 | return sortByName(caseName, cases).slice(0, 20)
62 | }
63 |
64 | const getPDF = (inputCase, inputDocumentType) => null
65 |
66 | const ICJCIJ = {
67 | getCaseByName,
68 | getPDF,
69 | }
70 |
71 | export default ICJCIJ
--------------------------------------------------------------------------------
/src/utils/scraper/UN/UN.ts:
--------------------------------------------------------------------------------
1 |
2 | import ICJCIJ from './ICJCIJ'
3 | import Constants from '../../Constants'
4 | import { databaseUseDatabase, databaseUseJurisdiction, makeEventTarget } from '../utils'
5 | import { sortUNCases } from 'utils/Finder/CaseCitationFinder/UN'
6 |
7 | const databaseUseUN = databaseUseJurisdiction(`UN`)
8 | const databaseUseICJCIJ = databaseUseDatabase(`icjcij`, databaseUseUN)
9 |
10 | const getCaseByName = (caseName: string): EventTarget => makeEventTarget(
11 | caseName,
12 | [
13 | databaseUseICJCIJ(() => ICJCIJ.getCaseByName(caseName)),
14 | ],
15 | `UN`,
16 | sortUNCases,
17 | true,
18 | )
19 |
20 | const getCaseByCitation = (
21 | citation: string,
22 | ): EventTarget => makeEventTarget(
23 | citation,
24 | [],
25 | `UN`,
26 | sortUNCases,
27 | false,
28 | )
29 |
30 | const databaseMap = {
31 | [Constants.DATABASES.UN_icjcij.id]: ICJCIJ,
32 | }
33 |
34 | const getPDF = async (
35 | inputCase: Law.Case,
36 | inputDocumentType: Law.Link[`doctype`],
37 | ): Promise => {
38 | const { database } = inputCase
39 | return databaseMap[database.id].getPDF(inputCase, inputDocumentType)
40 | }
41 |
42 | const UN = {
43 | getCaseByCitation,
44 | getCaseByName,
45 | getPDF,
46 | }
47 |
48 | export default UN
--------------------------------------------------------------------------------
/src/utils/scraper/UN/index.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export {default} from './UN'
--------------------------------------------------------------------------------
/src/utils/scraper/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { sortByName } from "../utils"
2 |
3 | type TestCase = {
4 | cases: string[],
5 | index: number,
6 | query: string,
7 | }
8 |
9 | const testCases: TestCase[] = [
10 | {
11 | cases: [
12 | `RANGE CONSTRUCTION PTE LTD v GOLDBELL ENGINEERING PTE LTD`,
13 | `Re: Aathar Ah Kong Andrew`,
14 | `Aathar Ah Kong Andrew v CIMB Securities (Singapore) Pte Ltd`,
15 | `Azman bin Kamis v Saag Oilfield Engineering (S) Pte Ltd (formerly known as Derrick Services Singapore Pte Ltd) and another suit`,
16 | `Lee Tat Development Pte Ltd v Management Corporation of Grange Heights Strata Title No 301 (No 2)`,
17 | ],
18 | index: 0,
19 | query: `range`,
20 | },
21 | {
22 | cases: [
23 | `UJF v UJG`,
24 | `Azman bin Kamis v Saag Oilfield Engineering (S) Pte Ltd (formerly known as Derrick Services Singapore Pte Ltd) and another suit`,
25 | `Tribune Investment Trust Inc v Soosan Trading Co Ltd`,
26 | `Lee Tat Development Pte Ltd v Management Corporation of Grange Heights Strata Title No 301 (No 2)`,
27 | ],
28 | index: 2,
29 | query: `tribune investment v soosan`,
30 | },
31 | {
32 | cases: [
33 | `Chua Choon Lim Robert v MN Swami and Others`,
34 | `Chua Teck Chew Robert v Goh Eng Wah`,
35 | `KOH CHEW CHEE v LIU SHU MING & Anor`,
36 | `Goh Nellie v Goh Lian Teck and Others`,
37 | ],
38 | index: 1,
39 | query: `Chua Teck Chew Robert v Goh Eng Wah`,
40 | },
41 | {
42 | cases: [
43 | `Choong Peng Kong v Koh Hong Son`,
44 | `China Airlines Ltd v Philips Hong Kong Ltd`,
45 | `China Airlines Limited v Philips Hong Kong Limited`,
46 | `Kong Chee Chui and others v Soh Ghee Hong`,
47 | `CRRC (HONG KONG) CO. LIMITED & Anor v CHEN WEIPING`,
48 | `Fustar Chemicals Ltd (Hong Kong) v Liquidator of Fustar Chemicals Pte Ltd`,
49 | `Fustar Chemicals Ltd v Ong Soo Hwa (liquidator of Fustar Chemicals Pte Ltd)`,
50 | ],
51 | index: 5,
52 | query: `Fustar Chemicals (Hong Kong)`,
53 | },
54 | {
55 | cases: [
56 | `PEX International Pte Ltd v Lim Seng Chye & Anor`,
57 | `Tan Chi Min v The Royal Bank of Scotland Plc`,
58 | `WestLB AG v Philippine National Bank & Others`,
59 | `Cousins Scott William v The Royal Bank of Scotland plc`,
60 | `The Royal Bank of Scotland NV (formerly known as ABN Amro Bank NV) and others v TT International Ltd and another appeal`,
61 | `The Royal Bank of Scotland NV (formerly known as ABN Amro Bank NV) and others v TT International Ltd (nTan Corporate Advisory Pte Ltd and others, other parties) and another appeal`,
62 | ],
63 | index: 4,
64 | query: `Royal Bank of Scotland v TT International`,
65 | },
66 | ]
67 |
68 | describe(`scraper utils`, () => {
69 |
70 | test.concurrent
71 | .each(testCases.map((test) => [test.query, test.cases, test.index]))(`sort case names: %s`, (query, cases, id) => {
72 | const sorted = sortByName(query, cases.map(c => ({ name: c }) as Law.Case))
73 | expect(sorted[0].name).toBe(cases[id])
74 | })
75 | })
--------------------------------------------------------------------------------
/src/utils/scraper/common/index.ts:
--------------------------------------------------------------------------------
1 | import CommonLII from './CommonLII'
2 |
3 | const common = {
4 | CommonLII,
5 | }
6 |
7 | export default common
--------------------------------------------------------------------------------
/src/utils/scraper/custom/Custom.ts:
--------------------------------------------------------------------------------
1 | import Fuse from 'fuse.js'
2 | import Helpers from 'utils/Helpers'
3 | import CustomDB from './CustomDB'
4 |
5 | const { cases: CustomCases } = CustomDB
6 |
7 | const toLawCase = ({ citations, ...others }: RawCase): Law.Case => ({
8 | ...others,
9 | citation: citations[0],
10 | })
11 |
12 | const getCaseByName = async (caseName: string): Promise => {
13 | const fuse = new Fuse(CustomCases.map(({ name }) => name), { ignoreLocation: true })
14 | return fuse
15 | .search(caseName)
16 | .map(({ refIndex }) => CustomCases[refIndex])
17 | .map(rawCase => toLawCase(rawCase))
18 | }
19 |
20 | const getCaseByCitation = async (citation: string, court: string): Promise => {
21 | const escapedCitation = Helpers.escapeRegExp(citation)
22 | return CustomCases
23 | .filter(({ citations }) =>
24 | citations.some((cit) => (new RegExp(`${escapedCitation}`, `i`)).test(cit)),
25 | )
26 | .map(({ citations, ...others }) => ({
27 | ...others,
28 | citation: citations[0],
29 | }))
30 | }
31 |
32 | const Custom = {
33 | getCaseByCitation,
34 | getCaseByName,
35 | }
36 |
37 | export default Custom
38 |
--------------------------------------------------------------------------------
/src/utils/scraper/custom/CustomDB.ts:
--------------------------------------------------------------------------------
1 | import Constants from "../../Constants"
2 |
3 | const CustomDBCases: RawCase[] = [
4 | {
5 | citations: [
6 | `[1957] 1 WLR 582`,
7 | `[1957] 2 All ER 118`,
8 | ],
9 | database: Constants.DATABASES.custom,
10 | jurisdiction: Constants.JURISDICTIONS.UK.id,
11 | links: [
12 | {
13 | doctype: `Judgment`,
14 | filetype: `HTML`,
15 | url: `https://web.archive.org/web/20160122042428/http://oxcheps.new.ox.ac.uk:80/casebook/Resources/BOLAMV_1%20DOC.pdf`,
16 | },
17 | {
18 | doctype: `Judgment`,
19 | filetype: `PDF`,
20 | url: `https://web.archive.org/web/20160122042428if_/http://oxcheps.new.ox.ac.uk:80/casebook/Resources/BOLAMV_1%20DOC.pdf`,
21 | },
22 | ],
23 | name: `Bolam v Friern Hospital Management Committee`,
24 | },
25 | ]
26 |
27 | const CustomDB = {
28 | cases: CustomDBCases,
29 | }
30 |
31 | export default CustomDB
--------------------------------------------------------------------------------
/src/utils/scraper/custom/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Custom'
--------------------------------------------------------------------------------
/src/utils/scraper/index.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export {default} from './Scraper'
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | `./src/**/*.{html,js,jsx,ts,tsx}`,
4 | `./views/**/*.html`,
5 | ],
6 | plugins: [],
7 | theme: {
8 | extend: {
9 | spacing: {
10 | 112: `28rem`,
11 | },
12 | },
13 | },
14 | }
--------------------------------------------------------------------------------
/tests/svgTransform.js:
--------------------------------------------------------------------------------
1 | const path = require(`path`)
2 |
3 | module.exports = {
4 | process(source, filename, config, options) {
5 | return {
6 | code: `module.exports = '${JSON.stringify({
7 | path: path.basename(filename),
8 | source,
9 | })}';`,
10 | }
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018", // ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'.
4 | "module": "commonjs", // Module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext",
9 | "ES2020.String"
10 | ],
11 | "declaration": false,
12 | "isolatedModules": true,
13 | /* Additional Checks */
14 | "useDefineForClassFields": true,
15 | "skipLibCheck": true,
16 | "moduleResolution": "node",
17 | "jsx": "preserve",
18 | "jsxFactory": "preact.h",
19 | "jsxFragmentFactory": "preact.Fragment",
20 | "allowSyntheticDefaultImports": true,
21 | "downlevelIteration": true,
22 | "esModuleInterop": true,
23 | "allowJs": false,
24 | "sourceMap": true,
25 | "baseUrl": "src",
26 | "paths": {
27 | "react": [
28 | "./node_modules/preact/compat/"
29 | ],
30 | "react-dom": [
31 | "./node_modules/preact/compat/"
32 | ]
33 | }
34 | },
35 | "include": [
36 | "src",
37 | "jest.config.ts.ts",
38 | "tailwind.config.js"
39 | ],
40 | "exclude": [
41 | "**/node_modules",
42 | "**/.*/"
43 | ]
44 | }
--------------------------------------------------------------------------------
/views/background.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Background Page
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/views/guide.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Guide | Clerkent
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/views/mass-citations.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Clerkent - Mass Citations
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/views/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Options
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/views/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Popup
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/views/updates.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Updates | Clerkent
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------