{"id":43784691,"url":"https://github.com/medelman17/eyecite-ts","last_synced_at":"2026-05-16T01:10:08.285Z","repository":{"id":336599677,"uuid":"1150319721","full_name":"medelman17/eyecite-ts","owner":"medelman17","description":"TypeScript legal citation extraction library with zero dependencies. Extract, resolve, and annotate citations from court opinions. Supports case citations, statutes, journals, parallel linking, short-form resolution, and more. 7KB core bundle.","archived":false,"fork":false,"pushed_at":"2026-04-19T02:59:32.000Z","size":1627,"stargazers_count":2,"open_issues_count":6,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-19T04:33:37.794Z","etag":null,"topics":["bluebook","case-law","citation-extraction","citation-parser","citations","court-opinions","eyecite","legal","legal-citations","legal-documents","legal-tech","nlp","tree-shakeable","typescript","zero-dependencies"],"latest_commit_sha":null,"homepage":"https://eyecite-ts.edel.sh/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/medelman17.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-05T06:15:47.000Z","updated_at":"2026-04-19T02:59:13.000Z","dependencies_parsed_at":null,"dependency_job_id":"cf9e12fa-82fd-4676-9fe8-b1c030f9a90b","html_url":"https://github.com/medelman17/eyecite-ts","commit_stats":null,"previous_names":["medelman17/eyecitets"],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/medelman17/eyecite-ts","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/medelman17%2Feyecite-ts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/medelman17%2Feyecite-ts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/medelman17%2Feyecite-ts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/medelman17%2Feyecite-ts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/medelman17","download_url":"https://codeload.github.com/medelman17/eyecite-ts/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/medelman17%2Feyecite-ts/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32022561,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T20:23:30.271Z","status":"online","status_checked_at":"2026-04-19T02:00:07.110Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bluebook","case-law","citation-extraction","citation-parser","citations","court-opinions","eyecite","legal","legal-citations","legal-documents","legal-tech","nlp","tree-shakeable","typescript","zero-dependencies"],"created_at":"2026-02-05T19:02:02.659Z","updated_at":"2026-04-19T21:12:16.312Z","avatar_url":"https://github.com/medelman17.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# eyecite-ts\n\n[![CI](https://github.com/medelman17/eyecite-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/medelman17/eyecite-ts/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/medelman17/eyecite-ts/branch/main/graph/badge.svg)](https://codecov.io/gh/medelman17/eyecite-ts)\n[![npm version](https://img.shields.io/npm/v/eyecite-ts.svg)](https://www.npmjs.com/package/eyecite-ts)\n[![npm bundle size](https://img.shields.io/bundlephobia/minzip/eyecite-ts)](https://bundlephobia.com/package/eyecite-ts)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n[![Node.js](https://img.shields.io/node/v/eyecite-ts.svg)](https://nodejs.org)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)\n[![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](https://www.npmjs.com/package/eyecite-ts)\n\nTypeScript legal citation extraction — a port of Python [eyecite](https://github.com/freelawproject/eyecite) with extended capabilities.\n\nExtract structured data from legal citations in court opinions, briefs, and legal documents. A citation like `500 F.2d 123 (9th Cir. 2020)` encodes a volume (500), reporter (Federal Reporter, 2nd Series), page (123), court (Ninth Circuit), and year. This library parses all of that into typed objects, resolves short-form references like \"Id.\" back to their antecedents, and can annotate the original text with HTML markup. Zero runtime dependencies, browser-compatible, ~20 KB brotli.\n\n## Installation\n\n```bash\nnpm install eyecite-ts\n```\n\n## Quick Start\n\nA complete extract → resolve → annotate workflow:\n\n```typescript\nimport { extractCitations } from \"eyecite-ts\"\nimport { annotate } from \"eyecite-ts/annotate\"\n\nconst text = `In Smith v. Jones, 500 F.2d 123 (9th Cir. 2020), the court\napplied 42 U.S.C. § 1983. Id. at 130. See also 123 Harv. L. Rev. 456 (2019).`\n\n// Step 1: Extract and resolve in one call\nconst citations = extractCitations(text, { resolve: true })\n\n// Step 2: Inspect results\nfor (const cite of citations) {\n  switch (cite.type) {\n    case \"case\":\n      console.log(cite.caseName, cite.reporter, cite.year)\n      // \"Smith v. Jones\" \"F.2d\" 2020\n      break\n    case \"statute\":\n      console.log(cite.title, cite.code, cite.section)\n      // 42 \"U.S.C.\" \"1983\"\n      break\n    case \"id\":\n      console.log(\"Id. resolves to index\", cite.resolution?.resolvedTo)\n      // Id. resolves to index 0\n      break\n    case \"journal\":\n      console.log(cite.journal, cite.volume, cite.page)\n      // \"Harv. L. Rev.\" 123 456\n      break\n  }\n}\n\n// Step 3: Annotate the original text\nconst result = annotate(text, citations, {\n  template: { before: '\u003ccite\u003e', after: '\u003c/cite\u003e' },\n})\nconsole.log(result.text)\n```\n\n## What It Extracts\n\n| Type | Example | Key Fields |\n|------|---------|------------|\n| `case` | `500 F.2d 123 (9th Cir. 2020)` | volume, reporter, page, court, year, caseName |\n| `statute` | `42 U.S.C. § 1983(a)(1)` | title, code, section, subsection, jurisdiction |\n| `constitutional` | `U.S. Const. amend. XIV, § 1` | jurisdiction, amendment, section, clause |\n| `journal` | `123 Harv. L. Rev. 456` | volume, journal, page, year |\n| `neutral` | `2020 WL 123456` | year, court, documentNumber |\n| `publicLaw` | `Pub. L. No. 117-263` | congress, lawNumber |\n| `federalRegister` | `87 Fed. Reg. 1234` | volume, page, year |\n| `statutesAtLarge` | `136 Stat. 4459` | volume, page, year |\n| `id` | `Id. at 125` | pincite |\n| `supra` | `Smith, supra, at 130` | partyName, pincite |\n| `shortFormCase` | `500 F.2d at 140` | volume, reporter, pincite |\n\nStatute coverage spans 52 jurisdictions (50 states + DC + federal). See the [Advanced Extraction Guide](docs/guides/advanced-extraction.md) for jurisdiction details.\n\n## Key Features\n\n### Case Names \u0026 Full Spans\n\nThe library backward-searches for party names and tracks full citation boundaries:\n\n```typescript\nconst text = \"In Smith v. Jones, 500 F.2d 123 (9th Cir. 2020) (en banc), the court held...\"\nconst [cite] = extractCitations(text)\n\nif (cite.type === \"case\") {\n  cite.caseName   // \"Smith v. Jones\"\n  cite.plaintiff  // \"Smith\"\n  cite.defendant  // \"Jones\"\n  cite.disposition // \"en banc\"\n  cite.span       // covers \"500 F.2d 123\" (citation core)\n  cite.fullSpan   // covers \"Smith v. Jones, 500 F.2d 123 (9th Cir. 2020) (en banc)\"\n}\n```\n\nProcedural prefixes like `In re`, `Ex parte`, and `Matter of` are recognized automatically.\n\n### Parallel Citations\n\nWhen multiple reporters cite the same case (common in older Supreme Court opinions), the library groups them automatically:\n\n```typescript\nconst text = \"See 410 U.S. 113, 93 S. Ct. 705, 35 L. Ed. 2d 147 (1973).\"\nconst citations = extractCitations(text)\n\ncitations[0].groupId // \"410-U.S.-113\"\ncitations[1].groupId // \"410-U.S.-113\" (same group)\ncitations[2].groupId // \"410-U.S.-113\" (same group)\n\n// Primary citation carries the linked array\nif (citations[0].type === \"case\") {\n  citations[0].parallelCitations\n  // [{ volume: 93, reporter: 'S. Ct.', page: 705 },\n  //  { volume: 35, reporter: 'L. Ed. 2d', page: 147 }]\n}\n```\n\n### Short-Form Resolution\n\nPass `{ resolve: true }` to link Id., supra, and short-form case citations to their full antecedents:\n\n```typescript\nconst text = `Smith v. Jones, 500 F.2d 123 (2020). Id. at 125.`\nconst citations = extractCitations(text, { resolve: true })\n\ncitations[1].resolution\n// { resolvedTo: 0, confidence: 1.0 }\n```\n\nThe resolver supports paragraph/section/footnote scope boundaries, fuzzy party name matching, and configurable thresholds. See the [Resolution Guide](docs/guides/resolution.md) for the power-user API.\n\n### Citation Annotation\n\nMark up citations with HTML using template or callback modes:\n\n```typescript\nimport { annotate } from \"eyecite-ts/annotate\"\n\nconst result = annotate(text, citations, {\n  template: { before: '\u003ccite\u003e', after: '\u003c/cite\u003e' },\n})\n// \"See Smith v. Jones, \u003ccite\u003e500 F.2d 123\u003c/cite\u003e (2020).\"\n```\n\nXSS auto-escape is enabled by default. Use `useFullSpan: true` to annotate from case name through closing parenthetical. See the [Annotation Guide](docs/guides/annotation.md) for callback mode and full options.\n\n### Confidence \u0026 Signals\n\nEach citation carries a `confidence` score (0-1) based on pattern match quality and reporter validation. Citations preceded by legal signals are tagged:\n\n```typescript\nconst text = \"See also Smith v. Jones, 500 F.2d 123 (2020).\"\nconst [cite] = extractCitations(text)\n\ncite.confidence // 0.85\ncite.signal     // \"see also\"\n```\n\n### Footnote Detection\n\nOpt-in feature that tags citations with their footnote context and enables zone-scoped resolution:\n\n```typescript\nconst citations = extractCitations(text, { detectFootnotes: true })\n\nfor (const cite of citations) {\n  if (cite.inFootnote) {\n    console.log(`Footnote ${cite.footnoteNumber}: ${cite.matchedText}`)\n  }\n}\n```\n\nSupports HTML footnote tags and plaintext footnote sections (separator + numbered markers). See the [Footnote Detection Guide](docs/guides/footnote-detection.md).\n\n## Type System\n\nAll citation types use a [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) on the `type` field:\n\n```typescript\nimport type { Citation, FullCaseCitation, StatuteCitation } from \"eyecite-ts\"\nimport { isFullCitation, isCaseCitation, assertUnreachable } from \"eyecite-ts\"\n\n// Type guards\nif (isCaseCitation(citation)) {\n  citation.reporter // typed as string\n}\n\n// Exhaustive switch\nswitch (citation.type) {\n  case \"case\": /* ... */ break\n  case \"statute\": /* ... */ break\n  // ... all 11 types\n  default: assertUnreachable(citation.type)\n}\n```\n\n`CitationOfType\u003c'case'\u003e` extracts the subtype: `CitationOfType\u003c'case'\u003e` = `FullCaseCitation`. See the [Type Reference](docs/api/types.md) for the full catalog.\n\n## Bundle Size\n\nThree entry points for tree-shaking:\n\n| Entry Point | Import | Size (brotli) |\n|-------------|--------|---------------|\n| Core extraction | `eyecite-ts` | ~20 KB |\n| Annotation | `eyecite-ts/annotate` | ~1.3 KB |\n| Reporter data | `eyecite-ts/data` | lazy-loaded |\n\nImport only what you need — the reporter database is loaded on first use, not at import time.\n\n## Comparison with Python eyecite\n\nEvery claim verified against [Python eyecite](https://github.com/freelawproject/eyecite) source code (April 2026).\n\n| Capability | Python eyecite | eyecite-ts | Notes |\n|---|---|---|---|\n| Case citations | Yes | Yes | Both extract volume/reporter/page/court/year |\n| Statute citations | Yes (all 50 states + DC + territories) | Yes (50 states + DC + federal) | Python uses `reporters-db`; TS uses built-in patterns |\n| Constitutional citations | No | Yes (U.S. + 50 states) | Dedicated type with article/amendment/section/clause |\n| Journal / law review | Yes | Yes | |\n| Neutral (WL/LEXIS) | Yes (as case citations) | Yes (dedicated type) | |\n| Short-form resolution | Yes | Yes | |\n| Case name extraction | Yes | Yes | Both use backward scanning |\n| Parallel citation linking | Partial (detection + metadata copy) | Yes (`groupId` + `parallelCitations`) | |\n| Full span tracking | Yes | Yes | TS carries dual clean/original positions |\n| Component spans | Minimal (pin cite only) | Yes (all fields) | |\n| Footnote detection | No | Yes | HTML + plaintext strategies |\n| Citation signals | No (stop words only) | Yes (extracted as metadata) | |\n| Annotation | Yes (HTML modes) | Yes (template/callback + XSS auto-escape) | |\n| Position mapping | Yes (diff-based) | Yes (incremental TransformationMap) | |\n| Type system | Class inheritance | Discriminated union | TS enables exhaustive switch |\n\neyecite-ts started as a port and has diverged. Both are capable citation extractors — eyecite-ts adds constitutional citations, footnote detection, citation signals, rich component spans, and a TypeScript-native type system, while Python eyecite has broader statute coverage via `reporters-db` and a mature ecosystem.\n\nComing from Python eyecite? See the [Migration Guide](docs/guides/migration-from-python.md).\n\n## Architecture\n\nCitations flow through a 4-stage pipeline: **clean → tokenize → extract → resolve**. Text cleaning builds a `TransformationMap` that tracks position shifts, so every citation carries dual coordinates (cleaned and original text). Resolution is optional and runs as a final pass.\n\nSee [ARCHITECTURE.md](ARCHITECTURE.md) for details.\n\n## Development\n\n```bash\npnpm install           # Install dependencies (corepack, pnpm 10)\npnpm test              # Run tests (vitest, watch mode)\npnpm exec vitest run   # Run tests once (1,748 tests, 72 files)\npnpm typecheck         # Type-check with tsc\npnpm build             # Build (ESM + CJS + DTS)\npnpm lint              # Lint with Biome\npnpm format            # Format with Biome\npnpm size              # Check bundle size limits\n```\n\nRequires Node.js \u003e= 18.0.0. See [ARCHITECTURE.md](ARCHITECTURE.md) for contributor orientation.\n\n## License\n\nMIT\n\n## Credits\n\nInspired by and ported from [eyecite](https://github.com/freelawproject/eyecite) (Python) by [Free Law Project](https://free.law/). This TypeScript implementation extends the original with constitutional citations, footnote detection, citation signals, parallel citation grouping, component spans, and a discriminated-union type system.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmedelman17%2Feyecite-ts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmedelman17%2Feyecite-ts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmedelman17%2Feyecite-ts/lists"}