eyecite-ts 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,19 +9,22 @@
9
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
10
10
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](https://www.npmjs.com/package/eyecite-ts)
11
11
 
12
- TypeScript legal citation extraction library — port of Python [eyecite](https://github.com/freelawproject/eyecite).
12
+ TypeScript legal citation extraction library — inspired by and extending Python [eyecite](https://github.com/freelawproject/eyecite).
13
13
 
14
14
  Extract, resolve, and annotate legal citations from court opinions and legal documents with zero runtime dependencies.
15
15
 
16
16
  ## Features
17
17
 
18
18
  - **Full citation extraction**: Case citations, statutes, journal articles, neutral citations, public laws, federal register
19
+ - **Case name & full span**: Backward search extracts case names ("Smith v. Jones", "In re Smith"), `fullSpan` covers case name through closing parenthetical
20
+ - **Parallel citation linking**: Automatic detection and grouping of comma-separated citations sharing a parenthetical (e.g., "410 U.S. 113, 93 S. Ct. 705 (1973)")
21
+ - **Complex parentheticals**: Unified parser handles court+year, full dates (Jan. 15, 2020 / January 15, 2020 / 1/15/2020), disposition (en banc, per curiam), and chained parentheticals
19
22
  - **Short-form resolution**: Id./Ibid., supra, and short-form case citations resolved to their full antecedents
20
23
  - **Reporter database**: 1,200+ reporters with variant matching and confidence scoring
21
24
  - **Citation annotation**: HTML markup with auto-escape XSS protection and position tracking
22
25
  - **Bundle optimization**: Tree-shakeable exports, lazy-loaded reporter data, separate entry points
23
26
  - **TypeScript native**: Discriminated unions, conditional types, type guards, full IntelliSense
24
- - **Zero dependencies**: No runtime dependencies, 4.4KB gzipped core bundle
27
+ - **Zero dependencies**: No runtime dependencies, 7KB gzipped core bundle
25
28
 
26
29
  ## Installation
27
30
 
@@ -34,7 +37,7 @@ npm install eyecite-ts
34
37
  ```typescript
35
38
  import { extractCitations } from 'eyecite-ts'
36
39
 
37
- const text = 'See Smith v. Jones, 500 F.2d 123 (9th Cir. 2020)'
40
+ const text = 'See Smith v. Jones, 500 F.2d 123 (9th Cir. Jan. 15, 2020)'
38
41
  const citations = extractCitations(text)
39
42
 
40
43
  console.log(citations[0])
@@ -45,8 +48,11 @@ console.log(citations[0])
45
48
  // page: 123,
46
49
  // court: '9th Cir.',
47
50
  // year: 2020,
51
+ // caseName: 'Smith v. Jones',
52
+ // date: { iso: '2020-01-15', parsed: { year: 2020, month: 1, day: 15 } },
48
53
  // confidence: 0.85,
49
- // span: { originalStart: 4, originalEnd: 48, cleanStart: 4, cleanEnd: 48 }
54
+ // span: { originalStart: 20, originalEnd: 33, ... },
55
+ // fullSpan: { originalStart: 4, originalEnd: 57, ... }
50
56
  // }
51
57
  ```
52
58
 
@@ -100,6 +106,97 @@ const citations = extractCitations(html, {
100
106
  })
101
107
  ```
102
108
 
109
+ ## Case Names & Full Spans
110
+
111
+ Case citations can include the case name and full citation boundaries:
112
+
113
+ ```typescript
114
+ const text = 'In Smith v. Jones, 500 F.2d 123 (9th Cir. 2020) (en banc), the court held...'
115
+ const citations = extractCitations(text)
116
+
117
+ if (citations[0].type === 'case') {
118
+ console.log(citations[0].caseName) // 'Smith v. Jones'
119
+ console.log(citations[0].disposition) // 'en banc'
120
+ console.log(citations[0].fullSpan) // covers "Smith v. Jones, 500 F.2d 123 (9th Cir. 2020) (en banc)"
121
+ console.log(citations[0].span) // covers "500 F.2d 123" only (citation core)
122
+ }
123
+ ```
124
+
125
+ Procedural prefixes are recognized automatically:
126
+
127
+ ```typescript
128
+ const text = 'In re Smith, 410 U.S. 113 (1973)'
129
+ // caseName: 'In re Smith'
130
+
131
+ const text2 = 'Ex parte Young, 209 U.S. 123 (1908)'
132
+ // caseName: 'Ex parte Young'
133
+ ```
134
+
135
+ ### Structured Dates
136
+
137
+ Parentheticals with full dates return structured date objects:
138
+
139
+ ```typescript
140
+ const text = '500 F.3d 100 (2d Cir. Jan. 15, 2020)'
141
+ // date: { iso: '2020-01-15', parsed: { year: 2020, month: 1, day: 15 } }
142
+
143
+ const text2 = '410 U.S. 113 (1973)'
144
+ // date: { iso: '1973', parsed: { year: 1973 } }
145
+ ```
146
+
147
+ Three date formats are supported: `Jan. 15, 2020`, `January 15, 2020`, and `1/15/2020`.
148
+
149
+ ### Blank Page Citations
150
+
151
+ Citations can reference blank pages using placeholder notation:
152
+
153
+ ```typescript
154
+ const text = '500 F.2d ___ (2020)'
155
+ const citations = extractCitations(text)
156
+
157
+ if (citations[0].type === 'case') {
158
+ console.log(citations[0].hasBlankPage) // true
159
+ console.log(citations[0].page) // undefined
160
+ }
161
+ ```
162
+
163
+ Both `___` (triple underscore) and `---` (triple dash) are recognized as blank page placeholders. These appear in slip opinions or unpublished decisions where the final reporter page number is not yet available.
164
+
165
+ ## Parallel Citations
166
+
167
+ When multiple case citations share the same parenthetical, they represent parallel citations for the same case in different reporters. The library automatically detects and groups them:
168
+
169
+ ```typescript
170
+ const text = 'See 410 U.S. 113, 93 S. Ct. 705, 35 L. Ed. 2d 147 (1973).'
171
+ const citations = extractCitations(text)
172
+
173
+ // Returns 3 citations, all linked by groupId
174
+ console.log(citations[0].groupId) // "410-U.S.-113"
175
+ console.log(citations[1].groupId) // "410-U.S.-113" (same group)
176
+ console.log(citations[2].groupId) // "410-U.S.-113" (same group)
177
+
178
+ // Primary citation (first in group) has parallelCitations array
179
+ if (citations[0].type === 'case') {
180
+ console.log(citations[0].parallelCitations)
181
+ // [
182
+ // { volume: 93, reporter: 'S. Ct.', page: 705 },
183
+ // { volume: 35, reporter: 'L. Ed. 2d', page: 147 }
184
+ // ]
185
+ }
186
+
187
+ // Secondary citations don't duplicate the array
188
+ console.log(citations[1].parallelCitations) // undefined
189
+ console.log(citations[2].parallelCitations) // undefined
190
+ ```
191
+
192
+ **Key points:**
193
+ - All citations in a parallel group share the same `groupId`
194
+ - Only the **first citation** (primary) has the `parallelCitations` array
195
+ - Secondary citations remain in the results array for individual processing
196
+ - Group ID format: `${volume}-${reporter}-${page}` (e.g., "410-U.S.-113")
197
+
198
+ Use `groupId` to identify which citations refer to the same case, or access `parallelCitations` on the primary to get all reporters at once.
199
+
103
200
  ## Resolving Short-Form Citations
104
201
 
105
202
  Short-form citations (Id., supra, short-form case) refer to earlier citations in the document. The resolution engine links them to their full antecedents.
@@ -221,6 +318,38 @@ const result = annotate(text, citations, {
221
318
  })
222
319
  ```
223
320
 
321
+ ### Annotating Full Spans
322
+
323
+ By default, annotation wraps only the citation core (volume-reporter-page). Use `useFullSpan` to annotate from the case name through the closing parenthetical:
324
+
325
+ ```typescript
326
+ const text = 'In Smith v. Jones, 500 F.2d 123 (9th Cir. 2020) (en banc), the court held...'
327
+ const citations = extractCitations(text)
328
+
329
+ // Default: annotates only "500 F.2d 123"
330
+ const coreOnly = annotate(text, citations, {
331
+ template: { before: '<cite>', after: '</cite>' }
332
+ })
333
+ // Result: "In Smith v. Jones, <cite>500 F.2d 123</cite> (9th Cir. 2020) (en banc), the court held..."
334
+
335
+ // With useFullSpan: annotates "Smith v. Jones, 500 F.2d 123 (9th Cir. 2020) (en banc)"
336
+ const fullSpan = annotate(text, citations, {
337
+ template: { before: '<cite>', after: '</cite>' },
338
+ useFullSpan: true
339
+ })
340
+ // Result: "In <cite>Smith v. Jones, 500 F.2d 123 (9th Cir. 2020) (en banc)</cite>, the court held..."
341
+ ```
342
+
343
+ Full span annotation covers:
344
+ - Case name (if present)
345
+ - Volume-reporter-page
346
+ - Court and date parenthetical
347
+ - Disposition parenthetical (en banc, per curiam)
348
+ - Chained parentheticals
349
+ - Subsequent history
350
+
351
+ Use `useFullSpan: true` when you want to highlight the entire citation as a unit, or `useFullSpan: false` (default) to annotate only the citation core for minimal markup.
352
+
224
353
  ## Reporter Validation
225
354
 
226
355
  Validate case citations against the reporters database:
@@ -305,9 +434,9 @@ Three entry points for optimal tree-shaking:
305
434
 
306
435
  | Entry Point | Import | Gzipped |
307
436
  |------------|--------|---------|
308
- | Core extraction | `eyecite-ts` | 4.4 KB |
309
- | Annotation | `eyecite-ts/annotate` | 0.5 KB |
310
- | Reporter data | `eyecite-ts/data` | 88.5 KB (lazy-loaded) |
437
+ | Core extraction | `eyecite-ts` | 7.0 KB |
438
+ | Annotation | `eyecite-ts/annotate` | 0.7 KB |
439
+ | Reporter data | `eyecite-ts/data` | 86.5 KB (lazy-loaded) |
311
440
 
312
441
  ```typescript
313
442
  import { extractCitations } from 'eyecite-ts' // Core only
@@ -340,7 +469,7 @@ pnpm lint # Lint with Biome
340
469
  pnpm format # Format with Biome
341
470
  ```
342
471
 
343
- 304 tests, 97% statement coverage, 91% branch coverage.
472
+ 527 tests across 22 test files.
344
473
 
345
474
  ## License
346
475
 
@@ -348,4 +477,4 @@ MIT
348
477
 
349
478
  ## Credits
350
479
 
351
- Ported from [eyecite](https://github.com/freelawproject/eyecite) (Python) by Free Law Project.
480
+ Inspired by [eyecite](https://github.com/freelawproject/eyecite) (Python) by Free Law Project. This TypeScript implementation adds parallel citation linking, party name extraction, full span tracking, and performance optimizations while maintaining compatibility with the original API design.
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e,t,i={}){let{useCleanText:a=!1,autoEscape:o=!0,template:s,callback:c}=i,l=[...t].sort((e,t)=>{let n=a?e.span.cleanStart:e.span.originalStart;return(a?t.span.cleanStart:t.span.originalStart)-n}),u=e,d=new Map,f=[];for(let t of l){let i=a?t.span.cleanStart:t.span.originalStart,l=a?t.span.cleanEnd:t.span.originalEnd;if(!a){let e=n(u,i,l);if(e===null){f.push(t);continue}i=e.start,l=e.end}let p=``;if(c)p=c(t,e.substring(Math.max(0,i-30),Math.min(e.length,l+30)));else if(s){let e=u.substring(i,l),t=o?r(e):e;p=s.before+t+s.after}else continue;u=u.slice(0,i)+p+u.slice(l),d.set(i,i)}return{text:u,positionMap:d,skipped:f}}function t(e,t){let n=t-1;for(;n>=0;){if(e[n]===`>`)return null;if(e[n]===`<`){let r=t;for(;r<e.length;){if(e[r]===`>`)return{tagStart:n,tagEnd:r+1};r++}return{tagStart:n,tagEnd:e.length}}n--}return null}function n(e,n,r){let i=n,a=r,o=t(e,n);o&&(i=o.tagStart);let s=t(e,r);return s&&(a=s.tagEnd),i>=a?null:{start:i,end:a}}function r(e){let t={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`,"/":`&#x2F;`};return e.replace(/[&<>"'/]/g,e=>t[e])}exports.annotate=e;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e,t,i={}){let{useCleanText:a=!1,autoEscape:o=!0,useFullSpan:s=!1,template:c,callback:l}=i,u=[...t].sort((e,t)=>{let n=a?e.span.cleanStart:e.span.originalStart;return(a?t.span.cleanStart:t.span.originalStart)-n}),d=e,f=new Map,p=[];for(let t of u){let i,u;if(s&&`fullSpan`in t&&t.fullSpan?(i=a?t.fullSpan.cleanStart:t.fullSpan.originalStart,u=a?t.fullSpan.cleanEnd:t.fullSpan.originalEnd):(i=a?t.span.cleanStart:t.span.originalStart,u=a?t.span.cleanEnd:t.span.originalEnd),!a){let e=n(d,i,u);if(e===null){p.push(t);continue}i=e.start,u=e.end}let m=``;if(l)m=l(t,e.substring(Math.max(0,i-30),Math.min(e.length,u+30)));else if(c){let e=d.substring(i,u),t=o?r(e):e;m=c.before+t+c.after}else continue;d=d.slice(0,i)+m+d.slice(u),f.set(i,i)}return{text:d,positionMap:f,skipped:p}}function t(e,t){let n=t-1;for(;n>=0;){if(e[n]===`>`)return null;if(e[n]===`<`){let r=t;for(;r<e.length;){if(e[r]===`>`)return{tagStart:n,tagEnd:r+1};r++}return{tagStart:n,tagEnd:e.length}}n--}return null}function n(e,n,r){let i=n,a=r,o=t(e,n);o&&(i=o.tagStart);let s=t(e,r);return s&&(a=s.tagEnd),i>=a?null:{start:i,end:a}}function r(e){let t={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`,"/":`&#x2F;`};return e.replace(/[&<>"'/]/g,e=>t[e])}exports.annotate=e;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/annotate/annotate.ts"],"sourcesContent":["import type { Citation } from '../types/citation'\nimport type { AnnotationOptions, AnnotationResult } from './types'\n\n/**\n * Annotate citations in text with custom markup.\n *\n * Supports two modes:\n * - **Template mode**: Simple before/after wrapping (set `options.template`)\n * - **Callback mode**: Custom logic with full citation context (set `options.callback`)\n *\n * Citations are processed in reverse order to avoid position shifts invalidating\n * subsequent annotations. Position tracking maps original positions to new positions\n * after markup insertion.\n *\n * @param text - Original or cleaned text to annotate\n * @param citations - Citations to mark up (from extraction pipeline)\n * @param options - Annotation configuration\n * @returns Annotated text with position mapping\n *\n * @example Template mode\n * ```typescript\n * const result = annotate(text, citations, {\n * template: { before: '<cite>', after: '</cite>' }\n * })\n * // Result: \"See <cite>500 F.2d 123</cite>\"\n * ```\n *\n * @example Callback mode\n * ```typescript\n * const result = annotate(text, citations, {\n * callback: (citation) => {\n * if (citation.type === 'case') {\n * return `<a href=\"/cases/${citation.volume}\">${citation.matchedText}</a>`\n * }\n * return citation.matchedText\n * }\n * })\n * ```\n *\n * @example Position tracking\n * ```typescript\n * const result = annotate(text, citations, { template: { before: '<mark>', after: '</mark>' } })\n * // result.positionMap tracks how positions shifted\n * const originalPos = 10\n * const newPos = result.positionMap.get(originalPos)\n * ```\n */\nexport function annotate<C extends Citation = Citation>(\n text: string,\n citations: C[],\n options: AnnotationOptions<C> = {}\n): AnnotationResult {\n const {\n useCleanText = false,\n autoEscape = true, // Secure by default\n template,\n callback,\n } = options\n\n // Sort reverse to avoid position shifts invalidating subsequent annotations\n const sorted = [...citations].sort((a, b) => {\n const aPos = useCleanText ? a.span.cleanStart : a.span.originalStart\n const bPos = useCleanText ? b.span.cleanStart : b.span.originalStart\n return bPos - aPos // Reverse for backward iteration\n })\n\n let result = text\n const positionMap = new Map<number, number>()\n const skipped: Citation[] = []\n\n for (const citation of sorted) {\n let start = useCleanText ? citation.span.cleanStart : citation.span.originalStart\n let end = useCleanText ? citation.span.cleanEnd : citation.span.originalEnd\n\n // Snap positions out of HTML tags when annotating original text\n if (!useCleanText) {\n const snapped = snapOutOfHtmlTags(result, start, end)\n if (snapped === null) {\n // Could not safely snap — skip this citation\n skipped.push(citation)\n continue\n }\n start = snapped.start\n end = snapped.end\n }\n\n let markup = ''\n\n if (callback) {\n // Callback mode: developer provides full logic\n const surrounding = text.substring(\n Math.max(0, start - 30),\n Math.min(text.length, end + 30)\n )\n markup = callback(citation, surrounding)\n } else if (template) {\n // Template mode: simple before/after wrapping\n const citationText = result.substring(start, end)\n const escaped = autoEscape ? escapeHtmlEntities(citationText) : citationText\n markup = template.before + escaped + template.after\n } else {\n // No annotation specified\n continue\n }\n\n // Insert annotation (working backwards preserves positions for later citations)\n result = result.slice(0, start) + markup + result.slice(end)\n\n // Track original position to new position (before this annotation was added)\n positionMap.set(start, start)\n }\n\n return { text: result, positionMap, skipped }\n}\n\n/**\n * Check if a position falls inside an HTML tag (between `<` and `>`).\n * Returns the index of the opening `<` if inside a tag, otherwise -1.\n */\nfunction findContainingTag(text: string, pos: number): { tagStart: number; tagEnd: number } | null {\n // Search backwards from pos for '<' without encountering '>' first\n let i = pos - 1\n while (i >= 0) {\n if (text[i] === '>') return null // Hit a tag close — we're outside\n if (text[i] === '<') {\n // Found opening '<' — now find the closing '>'\n let j = pos\n while (j < text.length) {\n if (text[j] === '>') return { tagStart: i, tagEnd: j + 1 }\n j++\n }\n // Unclosed tag — treat as inside\n return { tagStart: i, tagEnd: text.length }\n }\n i--\n }\n return null\n}\n\n/**\n * Snap annotation start/end positions to avoid landing inside HTML tags.\n *\n * If a position falls inside an HTML tag, it is moved:\n * - Start position: snapped to before the tag's `<`\n * - End position: snapped to after the tag's `>`\n *\n * Returns null if the positions can't be safely adjusted (e.g., entirely\n * within a single tag).\n */\nfunction snapOutOfHtmlTags(\n text: string,\n start: number,\n end: number,\n): { start: number; end: number } | null {\n let snappedStart = start\n let snappedEnd = end\n\n const startTag = findContainingTag(text, start)\n if (startTag) {\n snappedStart = startTag.tagStart\n }\n\n const endTag = findContainingTag(text, end)\n if (endTag) {\n snappedEnd = endTag.tagEnd\n }\n\n // Sanity check: start must come before end\n if (snappedStart >= snappedEnd) return null\n\n return { start: snappedStart, end: snappedEnd }\n}\n\n/**\n * Escape HTML entities to prevent XSS injection.\n *\n * Converts special HTML characters to their entity equivalents:\n * - `&` → `&amp;`\n * - `<` → `&lt;`\n * - `>` → `&gt;`\n * - `\"` → `&quot;`\n * - `'` → `&#39;`\n * - `/` → `&#x2F;`\n *\n * @param text - Text to escape\n * @returns Escaped text safe for HTML insertion\n */\nfunction escapeHtmlEntities(text: string): string {\n const map: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n '/': '&#x2F;',\n }\n return text.replace(/[&<>\"'/]/g, (char) => map[char])\n}\n"],"mappings":"mEA+CA,SAAgB,EACd,EACA,EACA,EAAgC,EAAE,CAChB,CAClB,GAAM,CACJ,eAAe,GACf,aAAa,GACb,WACA,YACE,EAGE,EAAS,CAAC,GAAG,EAAU,CAAC,MAAM,EAAG,IAAM,CAC3C,IAAM,EAAO,EAAe,EAAE,KAAK,WAAa,EAAE,KAAK,cAEvD,OADa,EAAe,EAAE,KAAK,WAAa,EAAE,KAAK,eACzC,GACd,CAEE,EAAS,EACP,EAAc,IAAI,IAClB,EAAsB,EAAE,CAE9B,IAAK,IAAM,KAAY,EAAQ,CAC7B,IAAI,EAAQ,EAAe,EAAS,KAAK,WAAa,EAAS,KAAK,cAChE,EAAM,EAAe,EAAS,KAAK,SAAW,EAAS,KAAK,YAGhE,GAAI,CAAC,EAAc,CACjB,IAAM,EAAU,EAAkB,EAAQ,EAAO,EAAI,CACrD,GAAI,IAAY,KAAM,CAEpB,EAAQ,KAAK,EAAS,CACtB,SAEF,EAAQ,EAAQ,MAChB,EAAM,EAAQ,IAGhB,IAAI,EAAS,GAEb,GAAI,EAMF,EAAS,EAAS,EAJE,EAAK,UACvB,KAAK,IAAI,EAAG,EAAQ,GAAG,CACvB,KAAK,IAAI,EAAK,OAAQ,EAAM,GAAG,CAChC,CACuC,SAC/B,EAAU,CAEnB,IAAM,EAAe,EAAO,UAAU,EAAO,EAAI,CAC3C,EAAU,EAAa,EAAmB,EAAa,CAAG,EAChE,EAAS,EAAS,OAAS,EAAU,EAAS,WAG9C,SAIF,EAAS,EAAO,MAAM,EAAG,EAAM,CAAG,EAAS,EAAO,MAAM,EAAI,CAG5D,EAAY,IAAI,EAAO,EAAM,CAG/B,MAAO,CAAE,KAAM,EAAQ,cAAa,UAAS,CAO/C,SAAS,EAAkB,EAAc,EAA0D,CAEjG,IAAI,EAAI,EAAM,EACd,KAAO,GAAK,GAAG,CACb,GAAI,EAAK,KAAO,IAAK,OAAO,KAC5B,GAAI,EAAK,KAAO,IAAK,CAEnB,IAAI,EAAI,EACR,KAAO,EAAI,EAAK,QAAQ,CACtB,GAAI,EAAK,KAAO,IAAK,MAAO,CAAE,SAAU,EAAG,OAAQ,EAAI,EAAG,CAC1D,IAGF,MAAO,CAAE,SAAU,EAAG,OAAQ,EAAK,OAAQ,CAE7C,IAEF,OAAO,KAaT,SAAS,EACP,EACA,EACA,EACuC,CACvC,IAAI,EAAe,EACf,EAAa,EAEX,EAAW,EAAkB,EAAM,EAAM,CAC3C,IACF,EAAe,EAAS,UAG1B,IAAM,EAAS,EAAkB,EAAM,EAAI,CAQ3C,OAPI,IACF,EAAa,EAAO,QAIlB,GAAgB,EAAmB,KAEhC,CAAE,MAAO,EAAc,IAAK,EAAY,CAiBjD,SAAS,EAAmB,EAAsB,CAChD,IAAM,EAA8B,CAClC,IAAK,QACL,IAAK,OACL,IAAK,OACL,IAAK,SACL,IAAK,QACL,IAAK,SACN,CACD,OAAO,EAAK,QAAQ,YAAc,GAAS,EAAI,GAAM"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/annotate/annotate.ts"],"sourcesContent":["import type { Citation } from '../types/citation'\nimport type { AnnotationOptions, AnnotationResult } from './types'\n\n/**\n * Annotate citations in text with custom markup.\n *\n * Supports two modes:\n * - **Template mode**: Simple before/after wrapping (set `options.template`)\n * - **Callback mode**: Custom logic with full citation context (set `options.callback`)\n *\n * Citations are processed in reverse order to avoid position shifts invalidating\n * subsequent annotations. Position tracking maps original positions to new positions\n * after markup insertion.\n *\n * @param text - Original or cleaned text to annotate\n * @param citations - Citations to mark up (from extraction pipeline)\n * @param options - Annotation configuration\n * @returns Annotated text with position mapping\n *\n * @example Template mode\n * ```typescript\n * const result = annotate(text, citations, {\n * template: { before: '<cite>', after: '</cite>' }\n * })\n * // Result: \"See <cite>500 F.2d 123</cite>\"\n * ```\n *\n * @example Callback mode\n * ```typescript\n * const result = annotate(text, citations, {\n * callback: (citation) => {\n * if (citation.type === 'case') {\n * return `<a href=\"/cases/${citation.volume}\">${citation.matchedText}</a>`\n * }\n * return citation.matchedText\n * }\n * })\n * ```\n *\n * @example Position tracking\n * ```typescript\n * const result = annotate(text, citations, { template: { before: '<mark>', after: '</mark>' } })\n * // result.positionMap tracks how positions shifted\n * const originalPos = 10\n * const newPos = result.positionMap.get(originalPos)\n * ```\n */\nexport function annotate<C extends Citation = Citation>(\n text: string,\n citations: C[],\n options: AnnotationOptions<C> = {}\n): AnnotationResult {\n const {\n useCleanText = false,\n autoEscape = true, // Secure by default\n useFullSpan = false, // Backward compatible default\n template,\n callback,\n } = options\n\n // Sort reverse to avoid position shifts invalidating subsequent annotations\n const sorted = [...citations].sort((a, b) => {\n const aPos = useCleanText ? a.span.cleanStart : a.span.originalStart\n const bPos = useCleanText ? b.span.cleanStart : b.span.originalStart\n return bPos - aPos // Reverse for backward iteration\n })\n\n let result = text\n const positionMap = new Map<number, number>()\n const skipped: Citation[] = []\n\n for (const citation of sorted) {\n // Determine which span to use\n let start: number\n let end: number\n\n if (useFullSpan && 'fullSpan' in citation && citation.fullSpan) {\n // Full span mode: case name through parenthetical\n start = useCleanText ? citation.fullSpan.cleanStart : citation.fullSpan.originalStart\n end = useCleanText ? citation.fullSpan.cleanEnd : citation.fullSpan.originalEnd\n } else {\n // Default mode: core citation only\n start = useCleanText ? citation.span.cleanStart : citation.span.originalStart\n end = useCleanText ? citation.span.cleanEnd : citation.span.originalEnd\n }\n\n // Snap positions out of HTML tags when annotating original text\n if (!useCleanText) {\n const snapped = snapOutOfHtmlTags(result, start, end)\n if (snapped === null) {\n // Could not safely snap — skip this citation\n skipped.push(citation)\n continue\n }\n start = snapped.start\n end = snapped.end\n }\n\n let markup = ''\n\n if (callback) {\n // Callback mode: developer provides full logic\n const surrounding = text.substring(\n Math.max(0, start - 30),\n Math.min(text.length, end + 30)\n )\n markup = callback(citation, surrounding)\n } else if (template) {\n // Template mode: simple before/after wrapping\n const citationText = result.substring(start, end)\n const escaped = autoEscape ? escapeHtmlEntities(citationText) : citationText\n markup = template.before + escaped + template.after\n } else {\n // No annotation specified\n continue\n }\n\n // Insert annotation (working backwards preserves positions for later citations)\n result = result.slice(0, start) + markup + result.slice(end)\n\n // Track original position to new position (before this annotation was added)\n positionMap.set(start, start)\n }\n\n return { text: result, positionMap, skipped }\n}\n\n/**\n * Check if a position falls inside an HTML tag (between `<` and `>`).\n * Returns the index of the opening `<` if inside a tag, otherwise -1.\n */\nfunction findContainingTag(text: string, pos: number): { tagStart: number; tagEnd: number } | null {\n // Search backwards from pos for '<' without encountering '>' first\n let i = pos - 1\n while (i >= 0) {\n if (text[i] === '>') return null // Hit a tag close — we're outside\n if (text[i] === '<') {\n // Found opening '<' — now find the closing '>'\n let j = pos\n while (j < text.length) {\n if (text[j] === '>') return { tagStart: i, tagEnd: j + 1 }\n j++\n }\n // Unclosed tag — treat as inside\n return { tagStart: i, tagEnd: text.length }\n }\n i--\n }\n return null\n}\n\n/**\n * Snap annotation start/end positions to avoid landing inside HTML tags.\n *\n * If a position falls inside an HTML tag, it is moved:\n * - Start position: snapped to before the tag's `<`\n * - End position: snapped to after the tag's `>`\n *\n * Returns null if the positions can't be safely adjusted (e.g., entirely\n * within a single tag).\n */\nfunction snapOutOfHtmlTags(\n text: string,\n start: number,\n end: number,\n): { start: number; end: number } | null {\n let snappedStart = start\n let snappedEnd = end\n\n const startTag = findContainingTag(text, start)\n if (startTag) {\n snappedStart = startTag.tagStart\n }\n\n const endTag = findContainingTag(text, end)\n if (endTag) {\n snappedEnd = endTag.tagEnd\n }\n\n // Sanity check: start must come before end\n if (snappedStart >= snappedEnd) return null\n\n return { start: snappedStart, end: snappedEnd }\n}\n\n/**\n * Escape HTML entities to prevent XSS injection.\n *\n * Converts special HTML characters to their entity equivalents:\n * - `&` → `&amp;`\n * - `<` → `&lt;`\n * - `>` → `&gt;`\n * - `\"` → `&quot;`\n * - `'` → `&#39;`\n * - `/` → `&#x2F;`\n *\n * @param text - Text to escape\n * @returns Escaped text safe for HTML insertion\n */\nfunction escapeHtmlEntities(text: string): string {\n const map: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n '/': '&#x2F;',\n }\n return text.replace(/[&<>\"'/]/g, (char) => map[char])\n}\n"],"mappings":"mEA+CA,SAAgB,EACd,EACA,EACA,EAAgC,EAAE,CAChB,CAClB,GAAM,CACJ,eAAe,GACf,aAAa,GACb,cAAc,GACd,WACA,YACE,EAGE,EAAS,CAAC,GAAG,EAAU,CAAC,MAAM,EAAG,IAAM,CAC3C,IAAM,EAAO,EAAe,EAAE,KAAK,WAAa,EAAE,KAAK,cAEvD,OADa,EAAe,EAAE,KAAK,WAAa,EAAE,KAAK,eACzC,GACd,CAEE,EAAS,EACP,EAAc,IAAI,IAClB,EAAsB,EAAE,CAE9B,IAAK,IAAM,KAAY,EAAQ,CAE7B,IAAI,EACA,EAaJ,GAXI,GAAe,aAAc,GAAY,EAAS,UAEpD,EAAQ,EAAe,EAAS,SAAS,WAAa,EAAS,SAAS,cACxE,EAAM,EAAe,EAAS,SAAS,SAAW,EAAS,SAAS,cAGpE,EAAQ,EAAe,EAAS,KAAK,WAAa,EAAS,KAAK,cAChE,EAAM,EAAe,EAAS,KAAK,SAAW,EAAS,KAAK,aAI1D,CAAC,EAAc,CACjB,IAAM,EAAU,EAAkB,EAAQ,EAAO,EAAI,CACrD,GAAI,IAAY,KAAM,CAEpB,EAAQ,KAAK,EAAS,CACtB,SAEF,EAAQ,EAAQ,MAChB,EAAM,EAAQ,IAGhB,IAAI,EAAS,GAEb,GAAI,EAMF,EAAS,EAAS,EAJE,EAAK,UACvB,KAAK,IAAI,EAAG,EAAQ,GAAG,CACvB,KAAK,IAAI,EAAK,OAAQ,EAAM,GAAG,CAChC,CACuC,SAC/B,EAAU,CAEnB,IAAM,EAAe,EAAO,UAAU,EAAO,EAAI,CAC3C,EAAU,EAAa,EAAmB,EAAa,CAAG,EAChE,EAAS,EAAS,OAAS,EAAU,EAAS,WAG9C,SAIF,EAAS,EAAO,MAAM,EAAG,EAAM,CAAG,EAAS,EAAO,MAAM,EAAI,CAG5D,EAAY,IAAI,EAAO,EAAM,CAG/B,MAAO,CAAE,KAAM,EAAQ,cAAa,UAAS,CAO/C,SAAS,EAAkB,EAAc,EAA0D,CAEjG,IAAI,EAAI,EAAM,EACd,KAAO,GAAK,GAAG,CACb,GAAI,EAAK,KAAO,IAAK,OAAO,KAC5B,GAAI,EAAK,KAAO,IAAK,CAEnB,IAAI,EAAI,EACR,KAAO,EAAI,EAAK,QAAQ,CACtB,GAAI,EAAK,KAAO,IAAK,MAAO,CAAE,SAAU,EAAG,OAAQ,EAAI,EAAG,CAC1D,IAGF,MAAO,CAAE,SAAU,EAAG,OAAQ,EAAK,OAAQ,CAE7C,IAEF,OAAO,KAaT,SAAS,EACP,EACA,EACA,EACuC,CACvC,IAAI,EAAe,EACf,EAAa,EAEX,EAAW,EAAkB,EAAM,EAAM,CAC3C,IACF,EAAe,EAAS,UAG1B,IAAM,EAAS,EAAkB,EAAM,EAAI,CAQ3C,OAPI,IACF,EAAa,EAAO,QAIlB,GAAgB,EAAmB,KAEhC,CAAE,MAAO,EAAc,IAAK,EAAY,CAiBjD,SAAS,EAAmB,EAAsB,CAChD,IAAM,EAA8B,CAClC,IAAK,QACL,IAAK,OACL,IAAK,OACL,IAAK,SACL,IAAK,QACL,IAAK,SACN,CACD,OAAO,EAAK,QAAQ,YAAc,GAAS,EAAI,GAAM"}
@@ -1,4 +1,4 @@
1
- import { t as Citation } from "../citation-qKSc_Myj.cjs";
1
+ import { t as Citation } from "../citation-4bmWbhSK.cjs";
2
2
 
3
3
  //#region src/annotate/types.d.ts
4
4
  /**
@@ -56,6 +56,19 @@ interface AnnotationOptions<C extends Citation = Citation> {
56
56
  */
57
57
  autoEscape?: boolean;
58
58
  /**
59
+ * Use full citation span from case name through parenthetical (true) or core citation only (false).
60
+ *
61
+ * When enabled and citation has a fullSpan field (from Phase 6+), annotation will span:
62
+ * - Case name: "Smith v. Jones"
63
+ * - Reporter: "500 F.2d 123"
64
+ * - Parenthetical: "(9th Cir. 1974)"
65
+ *
66
+ * When disabled or fullSpan unavailable, falls back to core citation span (volume-reporter-page).
67
+ *
68
+ * @default false (backward compatible)
69
+ */
70
+ useFullSpan?: boolean;
71
+ /**
59
72
  * Callback for custom annotation logic.
60
73
  *
61
74
  * Receives each citation and surrounding context (±30 characters),
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/annotate/types.ts","../../src/annotate/annotate.ts"],"mappings":";;;;;AA4BA;;;;;;;;;;;;;;;;;;;;;;AAoEA;;UApEiB,iBAAA,WAA4B,QAAA,GAAW,QAAA;EA0F7C;;;;;;;;EAjFT,YAAA;;;ACUF;;;;;;;;;;;;;;;EDSE,UAAA;;;;;;;;;;;EAYA,QAAA,IAAY,QAAA,EAAU,CAAA,EAAG,WAAA;;;;;;;;;;;;;;;;EAiBzB,QAAA;+CAEE,MAAA;IAEA,KAAA;EAAA;AAAA;;;;UAOa,gBAAA;;;;EAIf,IAAA;;;;;;;;;EAUA,WAAA,EAAa,GAAA;;;;;;;EAQb,OAAA,EAAS,QAAA;AAAA;;;;AA1FX;;;;;;;;;;;;;;;;;;;;;;AAoEA;;;;;;;;;;;;;;ACjDA;;;;;;;iBAAgB,QAAA,WAAmB,QAAA,GAAW,QAAA,CAAA,CAC5C,IAAA,UACA,SAAA,EAAW,CAAA,IACX,OAAA,GAAS,iBAAA,CAAkB,CAAA,IAC1B,gBAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/annotate/types.ts","../../src/annotate/annotate.ts"],"mappings":";;;;;AA4BA;;;;;;;;;;;;;;;;;;;;;;;AAkFA;UAlFiB,iBAAA,WAA4B,QAAA,GAAW,QAAA;;;;;;;;;EAStD,YAAA;;;;ACUF;;;;;;;;;;;;;;EDSE,UAAA;;;;;;;;;;;;;EAcA,WAAA;;;;;;;;;;;EAYA,QAAA,IAAY,QAAA,EAAU,CAAA,EAAG,WAAA;;;;;;;;;;;;;;;;EAiBzB,QAAA;+CAEE,MAAA;IAEA,KAAA;EAAA;AAAA;;;;UAOa,gBAAA;;;;EAIf,IAAA;;;;;;;;;EAUA,WAAA,EAAa,GAAA;;;;;;;EAQb,OAAA,EAAS,QAAA;AAAA;;;;AAxGX;;;;;;;;;;;;;;;;;;;;;;;AAkFA;;;;;;;;;;;;;;AC/DA;;;;;;iBAAgB,QAAA,WAAmB,QAAA,GAAW,QAAA,CAAA,CAC5C,IAAA,UACA,SAAA,EAAW,CAAA,IACX,OAAA,GAAS,iBAAA,CAAkB,CAAA,IAC1B,gBAAA"}
@@ -1,4 +1,4 @@
1
- import { t as Citation } from "../citation-DAyM8kNA.mjs";
1
+ import { t as Citation } from "../citation-BVN0o8TJ.mjs";
2
2
 
3
3
  //#region src/annotate/types.d.ts
4
4
  /**
@@ -56,6 +56,19 @@ interface AnnotationOptions<C extends Citation = Citation> {
56
56
  */
57
57
  autoEscape?: boolean;
58
58
  /**
59
+ * Use full citation span from case name through parenthetical (true) or core citation only (false).
60
+ *
61
+ * When enabled and citation has a fullSpan field (from Phase 6+), annotation will span:
62
+ * - Case name: "Smith v. Jones"
63
+ * - Reporter: "500 F.2d 123"
64
+ * - Parenthetical: "(9th Cir. 1974)"
65
+ *
66
+ * When disabled or fullSpan unavailable, falls back to core citation span (volume-reporter-page).
67
+ *
68
+ * @default false (backward compatible)
69
+ */
70
+ useFullSpan?: boolean;
71
+ /**
59
72
  * Callback for custom annotation logic.
60
73
  *
61
74
  * Receives each citation and surrounding context (±30 characters),
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/annotate/types.ts","../../src/annotate/annotate.ts"],"mappings":";;;;;AA4BA;;;;;;;;;;;;;;;;;;;;;;AAoEA;;UApEiB,iBAAA,WAA4B,QAAA,GAAW,QAAA;EA0F7C;;;;;;;;EAjFT,YAAA;;;ACUF;;;;;;;;;;;;;;;EDSE,UAAA;;;;;;;;;;;EAYA,QAAA,IAAY,QAAA,EAAU,CAAA,EAAG,WAAA;;;;;;;;;;;;;;;;EAiBzB,QAAA;+CAEE,MAAA;IAEA,KAAA;EAAA;AAAA;;;;UAOa,gBAAA;;;;EAIf,IAAA;;;;;;;;;EAUA,WAAA,EAAa,GAAA;;;;;;;EAQb,OAAA,EAAS,QAAA;AAAA;;;;AA1FX;;;;;;;;;;;;;;;;;;;;;;AAoEA;;;;;;;;;;;;;;ACjDA;;;;;;;iBAAgB,QAAA,WAAmB,QAAA,GAAW,QAAA,CAAA,CAC5C,IAAA,UACA,SAAA,EAAW,CAAA,IACX,OAAA,GAAS,iBAAA,CAAkB,CAAA,IAC1B,gBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/annotate/types.ts","../../src/annotate/annotate.ts"],"mappings":";;;;;AA4BA;;;;;;;;;;;;;;;;;;;;;;;AAkFA;UAlFiB,iBAAA,WAA4B,QAAA,GAAW,QAAA;;;;;;;;;EAStD,YAAA;;;;ACUF;;;;;;;;;;;;;;EDSE,UAAA;;;;;;;;;;;;;EAcA,WAAA;;;;;;;;;;;EAYA,QAAA,IAAY,QAAA,EAAU,CAAA,EAAG,WAAA;;;;;;;;;;;;;;;;EAiBzB,QAAA;+CAEE,MAAA;IAEA,KAAA;EAAA;AAAA;;;;UAOa,gBAAA;;;;EAIf,IAAA;;;;;;;;;EAUA,WAAA,EAAa,GAAA;;;;;;;EAQb,OAAA,EAAS,QAAA;AAAA;;;;AAxGX;;;;;;;;;;;;;;;;;;;;;;;AAkFA;;;;;;;;;;;;;;AC/DA;;;;;;iBAAgB,QAAA,WAAmB,QAAA,GAAW,QAAA,CAAA,CAC5C,IAAA,UACA,SAAA,EAAW,CAAA,IACX,OAAA,GAAS,iBAAA,CAAkB,CAAA,IAC1B,gBAAA"}
@@ -1,2 +1,2 @@
1
- function e(e,t,i={}){let{useCleanText:a=!1,autoEscape:o=!0,template:s,callback:c}=i,l=[...t].sort((e,t)=>{let n=a?e.span.cleanStart:e.span.originalStart;return(a?t.span.cleanStart:t.span.originalStart)-n}),u=e,d=new Map,f=[];for(let t of l){let i=a?t.span.cleanStart:t.span.originalStart,l=a?t.span.cleanEnd:t.span.originalEnd;if(!a){let e=n(u,i,l);if(e===null){f.push(t);continue}i=e.start,l=e.end}let p=``;if(c)p=c(t,e.substring(Math.max(0,i-30),Math.min(e.length,l+30)));else if(s){let e=u.substring(i,l),t=o?r(e):e;p=s.before+t+s.after}else continue;u=u.slice(0,i)+p+u.slice(l),d.set(i,i)}return{text:u,positionMap:d,skipped:f}}function t(e,t){let n=t-1;for(;n>=0;){if(e[n]===`>`)return null;if(e[n]===`<`){let r=t;for(;r<e.length;){if(e[r]===`>`)return{tagStart:n,tagEnd:r+1};r++}return{tagStart:n,tagEnd:e.length}}n--}return null}function n(e,n,r){let i=n,a=r,o=t(e,n);o&&(i=o.tagStart);let s=t(e,r);return s&&(a=s.tagEnd),i>=a?null:{start:i,end:a}}function r(e){let t={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`,"/":`&#x2F;`};return e.replace(/[&<>"'/]/g,e=>t[e])}export{e as annotate};
1
+ function e(e,t,i={}){let{useCleanText:a=!1,autoEscape:o=!0,useFullSpan:s=!1,template:c,callback:l}=i,u=[...t].sort((e,t)=>{let n=a?e.span.cleanStart:e.span.originalStart;return(a?t.span.cleanStart:t.span.originalStart)-n}),d=e,f=new Map,p=[];for(let t of u){let i,u;if(s&&`fullSpan`in t&&t.fullSpan?(i=a?t.fullSpan.cleanStart:t.fullSpan.originalStart,u=a?t.fullSpan.cleanEnd:t.fullSpan.originalEnd):(i=a?t.span.cleanStart:t.span.originalStart,u=a?t.span.cleanEnd:t.span.originalEnd),!a){let e=n(d,i,u);if(e===null){p.push(t);continue}i=e.start,u=e.end}let m=``;if(l)m=l(t,e.substring(Math.max(0,i-30),Math.min(e.length,u+30)));else if(c){let e=d.substring(i,u),t=o?r(e):e;m=c.before+t+c.after}else continue;d=d.slice(0,i)+m+d.slice(u),f.set(i,i)}return{text:d,positionMap:f,skipped:p}}function t(e,t){let n=t-1;for(;n>=0;){if(e[n]===`>`)return null;if(e[n]===`<`){let r=t;for(;r<e.length;){if(e[r]===`>`)return{tagStart:n,tagEnd:r+1};r++}return{tagStart:n,tagEnd:e.length}}n--}return null}function n(e,n,r){let i=n,a=r,o=t(e,n);o&&(i=o.tagStart);let s=t(e,r);return s&&(a=s.tagEnd),i>=a?null:{start:i,end:a}}function r(e){let t={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`,"/":`&#x2F;`};return e.replace(/[&<>"'/]/g,e=>t[e])}export{e as annotate};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/annotate/annotate.ts"],"sourcesContent":["import type { Citation } from '../types/citation'\nimport type { AnnotationOptions, AnnotationResult } from './types'\n\n/**\n * Annotate citations in text with custom markup.\n *\n * Supports two modes:\n * - **Template mode**: Simple before/after wrapping (set `options.template`)\n * - **Callback mode**: Custom logic with full citation context (set `options.callback`)\n *\n * Citations are processed in reverse order to avoid position shifts invalidating\n * subsequent annotations. Position tracking maps original positions to new positions\n * after markup insertion.\n *\n * @param text - Original or cleaned text to annotate\n * @param citations - Citations to mark up (from extraction pipeline)\n * @param options - Annotation configuration\n * @returns Annotated text with position mapping\n *\n * @example Template mode\n * ```typescript\n * const result = annotate(text, citations, {\n * template: { before: '<cite>', after: '</cite>' }\n * })\n * // Result: \"See <cite>500 F.2d 123</cite>\"\n * ```\n *\n * @example Callback mode\n * ```typescript\n * const result = annotate(text, citations, {\n * callback: (citation) => {\n * if (citation.type === 'case') {\n * return `<a href=\"/cases/${citation.volume}\">${citation.matchedText}</a>`\n * }\n * return citation.matchedText\n * }\n * })\n * ```\n *\n * @example Position tracking\n * ```typescript\n * const result = annotate(text, citations, { template: { before: '<mark>', after: '</mark>' } })\n * // result.positionMap tracks how positions shifted\n * const originalPos = 10\n * const newPos = result.positionMap.get(originalPos)\n * ```\n */\nexport function annotate<C extends Citation = Citation>(\n text: string,\n citations: C[],\n options: AnnotationOptions<C> = {}\n): AnnotationResult {\n const {\n useCleanText = false,\n autoEscape = true, // Secure by default\n template,\n callback,\n } = options\n\n // Sort reverse to avoid position shifts invalidating subsequent annotations\n const sorted = [...citations].sort((a, b) => {\n const aPos = useCleanText ? a.span.cleanStart : a.span.originalStart\n const bPos = useCleanText ? b.span.cleanStart : b.span.originalStart\n return bPos - aPos // Reverse for backward iteration\n })\n\n let result = text\n const positionMap = new Map<number, number>()\n const skipped: Citation[] = []\n\n for (const citation of sorted) {\n let start = useCleanText ? citation.span.cleanStart : citation.span.originalStart\n let end = useCleanText ? citation.span.cleanEnd : citation.span.originalEnd\n\n // Snap positions out of HTML tags when annotating original text\n if (!useCleanText) {\n const snapped = snapOutOfHtmlTags(result, start, end)\n if (snapped === null) {\n // Could not safely snap — skip this citation\n skipped.push(citation)\n continue\n }\n start = snapped.start\n end = snapped.end\n }\n\n let markup = ''\n\n if (callback) {\n // Callback mode: developer provides full logic\n const surrounding = text.substring(\n Math.max(0, start - 30),\n Math.min(text.length, end + 30)\n )\n markup = callback(citation, surrounding)\n } else if (template) {\n // Template mode: simple before/after wrapping\n const citationText = result.substring(start, end)\n const escaped = autoEscape ? escapeHtmlEntities(citationText) : citationText\n markup = template.before + escaped + template.after\n } else {\n // No annotation specified\n continue\n }\n\n // Insert annotation (working backwards preserves positions for later citations)\n result = result.slice(0, start) + markup + result.slice(end)\n\n // Track original position to new position (before this annotation was added)\n positionMap.set(start, start)\n }\n\n return { text: result, positionMap, skipped }\n}\n\n/**\n * Check if a position falls inside an HTML tag (between `<` and `>`).\n * Returns the index of the opening `<` if inside a tag, otherwise -1.\n */\nfunction findContainingTag(text: string, pos: number): { tagStart: number; tagEnd: number } | null {\n // Search backwards from pos for '<' without encountering '>' first\n let i = pos - 1\n while (i >= 0) {\n if (text[i] === '>') return null // Hit a tag close — we're outside\n if (text[i] === '<') {\n // Found opening '<' — now find the closing '>'\n let j = pos\n while (j < text.length) {\n if (text[j] === '>') return { tagStart: i, tagEnd: j + 1 }\n j++\n }\n // Unclosed tag — treat as inside\n return { tagStart: i, tagEnd: text.length }\n }\n i--\n }\n return null\n}\n\n/**\n * Snap annotation start/end positions to avoid landing inside HTML tags.\n *\n * If a position falls inside an HTML tag, it is moved:\n * - Start position: snapped to before the tag's `<`\n * - End position: snapped to after the tag's `>`\n *\n * Returns null if the positions can't be safely adjusted (e.g., entirely\n * within a single tag).\n */\nfunction snapOutOfHtmlTags(\n text: string,\n start: number,\n end: number,\n): { start: number; end: number } | null {\n let snappedStart = start\n let snappedEnd = end\n\n const startTag = findContainingTag(text, start)\n if (startTag) {\n snappedStart = startTag.tagStart\n }\n\n const endTag = findContainingTag(text, end)\n if (endTag) {\n snappedEnd = endTag.tagEnd\n }\n\n // Sanity check: start must come before end\n if (snappedStart >= snappedEnd) return null\n\n return { start: snappedStart, end: snappedEnd }\n}\n\n/**\n * Escape HTML entities to prevent XSS injection.\n *\n * Converts special HTML characters to their entity equivalents:\n * - `&` → `&amp;`\n * - `<` → `&lt;`\n * - `>` → `&gt;`\n * - `\"` → `&quot;`\n * - `'` → `&#39;`\n * - `/` → `&#x2F;`\n *\n * @param text - Text to escape\n * @returns Escaped text safe for HTML insertion\n */\nfunction escapeHtmlEntities(text: string): string {\n const map: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n '/': '&#x2F;',\n }\n return text.replace(/[&<>\"'/]/g, (char) => map[char])\n}\n"],"mappings":"AA+CA,SAAgB,EACd,EACA,EACA,EAAgC,EAAE,CAChB,CAClB,GAAM,CACJ,eAAe,GACf,aAAa,GACb,WACA,YACE,EAGE,EAAS,CAAC,GAAG,EAAU,CAAC,MAAM,EAAG,IAAM,CAC3C,IAAM,EAAO,EAAe,EAAE,KAAK,WAAa,EAAE,KAAK,cAEvD,OADa,EAAe,EAAE,KAAK,WAAa,EAAE,KAAK,eACzC,GACd,CAEE,EAAS,EACP,EAAc,IAAI,IAClB,EAAsB,EAAE,CAE9B,IAAK,IAAM,KAAY,EAAQ,CAC7B,IAAI,EAAQ,EAAe,EAAS,KAAK,WAAa,EAAS,KAAK,cAChE,EAAM,EAAe,EAAS,KAAK,SAAW,EAAS,KAAK,YAGhE,GAAI,CAAC,EAAc,CACjB,IAAM,EAAU,EAAkB,EAAQ,EAAO,EAAI,CACrD,GAAI,IAAY,KAAM,CAEpB,EAAQ,KAAK,EAAS,CACtB,SAEF,EAAQ,EAAQ,MAChB,EAAM,EAAQ,IAGhB,IAAI,EAAS,GAEb,GAAI,EAMF,EAAS,EAAS,EAJE,EAAK,UACvB,KAAK,IAAI,EAAG,EAAQ,GAAG,CACvB,KAAK,IAAI,EAAK,OAAQ,EAAM,GAAG,CAChC,CACuC,SAC/B,EAAU,CAEnB,IAAM,EAAe,EAAO,UAAU,EAAO,EAAI,CAC3C,EAAU,EAAa,EAAmB,EAAa,CAAG,EAChE,EAAS,EAAS,OAAS,EAAU,EAAS,WAG9C,SAIF,EAAS,EAAO,MAAM,EAAG,EAAM,CAAG,EAAS,EAAO,MAAM,EAAI,CAG5D,EAAY,IAAI,EAAO,EAAM,CAG/B,MAAO,CAAE,KAAM,EAAQ,cAAa,UAAS,CAO/C,SAAS,EAAkB,EAAc,EAA0D,CAEjG,IAAI,EAAI,EAAM,EACd,KAAO,GAAK,GAAG,CACb,GAAI,EAAK,KAAO,IAAK,OAAO,KAC5B,GAAI,EAAK,KAAO,IAAK,CAEnB,IAAI,EAAI,EACR,KAAO,EAAI,EAAK,QAAQ,CACtB,GAAI,EAAK,KAAO,IAAK,MAAO,CAAE,SAAU,EAAG,OAAQ,EAAI,EAAG,CAC1D,IAGF,MAAO,CAAE,SAAU,EAAG,OAAQ,EAAK,OAAQ,CAE7C,IAEF,OAAO,KAaT,SAAS,EACP,EACA,EACA,EACuC,CACvC,IAAI,EAAe,EACf,EAAa,EAEX,EAAW,EAAkB,EAAM,EAAM,CAC3C,IACF,EAAe,EAAS,UAG1B,IAAM,EAAS,EAAkB,EAAM,EAAI,CAQ3C,OAPI,IACF,EAAa,EAAO,QAIlB,GAAgB,EAAmB,KAEhC,CAAE,MAAO,EAAc,IAAK,EAAY,CAiBjD,SAAS,EAAmB,EAAsB,CAChD,IAAM,EAA8B,CAClC,IAAK,QACL,IAAK,OACL,IAAK,OACL,IAAK,SACL,IAAK,QACL,IAAK,SACN,CACD,OAAO,EAAK,QAAQ,YAAc,GAAS,EAAI,GAAM"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/annotate/annotate.ts"],"sourcesContent":["import type { Citation } from '../types/citation'\nimport type { AnnotationOptions, AnnotationResult } from './types'\n\n/**\n * Annotate citations in text with custom markup.\n *\n * Supports two modes:\n * - **Template mode**: Simple before/after wrapping (set `options.template`)\n * - **Callback mode**: Custom logic with full citation context (set `options.callback`)\n *\n * Citations are processed in reverse order to avoid position shifts invalidating\n * subsequent annotations. Position tracking maps original positions to new positions\n * after markup insertion.\n *\n * @param text - Original or cleaned text to annotate\n * @param citations - Citations to mark up (from extraction pipeline)\n * @param options - Annotation configuration\n * @returns Annotated text with position mapping\n *\n * @example Template mode\n * ```typescript\n * const result = annotate(text, citations, {\n * template: { before: '<cite>', after: '</cite>' }\n * })\n * // Result: \"See <cite>500 F.2d 123</cite>\"\n * ```\n *\n * @example Callback mode\n * ```typescript\n * const result = annotate(text, citations, {\n * callback: (citation) => {\n * if (citation.type === 'case') {\n * return `<a href=\"/cases/${citation.volume}\">${citation.matchedText}</a>`\n * }\n * return citation.matchedText\n * }\n * })\n * ```\n *\n * @example Position tracking\n * ```typescript\n * const result = annotate(text, citations, { template: { before: '<mark>', after: '</mark>' } })\n * // result.positionMap tracks how positions shifted\n * const originalPos = 10\n * const newPos = result.positionMap.get(originalPos)\n * ```\n */\nexport function annotate<C extends Citation = Citation>(\n text: string,\n citations: C[],\n options: AnnotationOptions<C> = {}\n): AnnotationResult {\n const {\n useCleanText = false,\n autoEscape = true, // Secure by default\n useFullSpan = false, // Backward compatible default\n template,\n callback,\n } = options\n\n // Sort reverse to avoid position shifts invalidating subsequent annotations\n const sorted = [...citations].sort((a, b) => {\n const aPos = useCleanText ? a.span.cleanStart : a.span.originalStart\n const bPos = useCleanText ? b.span.cleanStart : b.span.originalStart\n return bPos - aPos // Reverse for backward iteration\n })\n\n let result = text\n const positionMap = new Map<number, number>()\n const skipped: Citation[] = []\n\n for (const citation of sorted) {\n // Determine which span to use\n let start: number\n let end: number\n\n if (useFullSpan && 'fullSpan' in citation && citation.fullSpan) {\n // Full span mode: case name through parenthetical\n start = useCleanText ? citation.fullSpan.cleanStart : citation.fullSpan.originalStart\n end = useCleanText ? citation.fullSpan.cleanEnd : citation.fullSpan.originalEnd\n } else {\n // Default mode: core citation only\n start = useCleanText ? citation.span.cleanStart : citation.span.originalStart\n end = useCleanText ? citation.span.cleanEnd : citation.span.originalEnd\n }\n\n // Snap positions out of HTML tags when annotating original text\n if (!useCleanText) {\n const snapped = snapOutOfHtmlTags(result, start, end)\n if (snapped === null) {\n // Could not safely snap — skip this citation\n skipped.push(citation)\n continue\n }\n start = snapped.start\n end = snapped.end\n }\n\n let markup = ''\n\n if (callback) {\n // Callback mode: developer provides full logic\n const surrounding = text.substring(\n Math.max(0, start - 30),\n Math.min(text.length, end + 30)\n )\n markup = callback(citation, surrounding)\n } else if (template) {\n // Template mode: simple before/after wrapping\n const citationText = result.substring(start, end)\n const escaped = autoEscape ? escapeHtmlEntities(citationText) : citationText\n markup = template.before + escaped + template.after\n } else {\n // No annotation specified\n continue\n }\n\n // Insert annotation (working backwards preserves positions for later citations)\n result = result.slice(0, start) + markup + result.slice(end)\n\n // Track original position to new position (before this annotation was added)\n positionMap.set(start, start)\n }\n\n return { text: result, positionMap, skipped }\n}\n\n/**\n * Check if a position falls inside an HTML tag (between `<` and `>`).\n * Returns the index of the opening `<` if inside a tag, otherwise -1.\n */\nfunction findContainingTag(text: string, pos: number): { tagStart: number; tagEnd: number } | null {\n // Search backwards from pos for '<' without encountering '>' first\n let i = pos - 1\n while (i >= 0) {\n if (text[i] === '>') return null // Hit a tag close — we're outside\n if (text[i] === '<') {\n // Found opening '<' — now find the closing '>'\n let j = pos\n while (j < text.length) {\n if (text[j] === '>') return { tagStart: i, tagEnd: j + 1 }\n j++\n }\n // Unclosed tag — treat as inside\n return { tagStart: i, tagEnd: text.length }\n }\n i--\n }\n return null\n}\n\n/**\n * Snap annotation start/end positions to avoid landing inside HTML tags.\n *\n * If a position falls inside an HTML tag, it is moved:\n * - Start position: snapped to before the tag's `<`\n * - End position: snapped to after the tag's `>`\n *\n * Returns null if the positions can't be safely adjusted (e.g., entirely\n * within a single tag).\n */\nfunction snapOutOfHtmlTags(\n text: string,\n start: number,\n end: number,\n): { start: number; end: number } | null {\n let snappedStart = start\n let snappedEnd = end\n\n const startTag = findContainingTag(text, start)\n if (startTag) {\n snappedStart = startTag.tagStart\n }\n\n const endTag = findContainingTag(text, end)\n if (endTag) {\n snappedEnd = endTag.tagEnd\n }\n\n // Sanity check: start must come before end\n if (snappedStart >= snappedEnd) return null\n\n return { start: snappedStart, end: snappedEnd }\n}\n\n/**\n * Escape HTML entities to prevent XSS injection.\n *\n * Converts special HTML characters to their entity equivalents:\n * - `&` → `&amp;`\n * - `<` → `&lt;`\n * - `>` → `&gt;`\n * - `\"` → `&quot;`\n * - `'` → `&#39;`\n * - `/` → `&#x2F;`\n *\n * @param text - Text to escape\n * @returns Escaped text safe for HTML insertion\n */\nfunction escapeHtmlEntities(text: string): string {\n const map: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n '/': '&#x2F;',\n }\n return text.replace(/[&<>\"'/]/g, (char) => map[char])\n}\n"],"mappings":"AA+CA,SAAgB,EACd,EACA,EACA,EAAgC,EAAE,CAChB,CAClB,GAAM,CACJ,eAAe,GACf,aAAa,GACb,cAAc,GACd,WACA,YACE,EAGE,EAAS,CAAC,GAAG,EAAU,CAAC,MAAM,EAAG,IAAM,CAC3C,IAAM,EAAO,EAAe,EAAE,KAAK,WAAa,EAAE,KAAK,cAEvD,OADa,EAAe,EAAE,KAAK,WAAa,EAAE,KAAK,eACzC,GACd,CAEE,EAAS,EACP,EAAc,IAAI,IAClB,EAAsB,EAAE,CAE9B,IAAK,IAAM,KAAY,EAAQ,CAE7B,IAAI,EACA,EAaJ,GAXI,GAAe,aAAc,GAAY,EAAS,UAEpD,EAAQ,EAAe,EAAS,SAAS,WAAa,EAAS,SAAS,cACxE,EAAM,EAAe,EAAS,SAAS,SAAW,EAAS,SAAS,cAGpE,EAAQ,EAAe,EAAS,KAAK,WAAa,EAAS,KAAK,cAChE,EAAM,EAAe,EAAS,KAAK,SAAW,EAAS,KAAK,aAI1D,CAAC,EAAc,CACjB,IAAM,EAAU,EAAkB,EAAQ,EAAO,EAAI,CACrD,GAAI,IAAY,KAAM,CAEpB,EAAQ,KAAK,EAAS,CACtB,SAEF,EAAQ,EAAQ,MAChB,EAAM,EAAQ,IAGhB,IAAI,EAAS,GAEb,GAAI,EAMF,EAAS,EAAS,EAJE,EAAK,UACvB,KAAK,IAAI,EAAG,EAAQ,GAAG,CACvB,KAAK,IAAI,EAAK,OAAQ,EAAM,GAAG,CAChC,CACuC,SAC/B,EAAU,CAEnB,IAAM,EAAe,EAAO,UAAU,EAAO,EAAI,CAC3C,EAAU,EAAa,EAAmB,EAAa,CAAG,EAChE,EAAS,EAAS,OAAS,EAAU,EAAS,WAG9C,SAIF,EAAS,EAAO,MAAM,EAAG,EAAM,CAAG,EAAS,EAAO,MAAM,EAAI,CAG5D,EAAY,IAAI,EAAO,EAAM,CAG/B,MAAO,CAAE,KAAM,EAAQ,cAAa,UAAS,CAO/C,SAAS,EAAkB,EAAc,EAA0D,CAEjG,IAAI,EAAI,EAAM,EACd,KAAO,GAAK,GAAG,CACb,GAAI,EAAK,KAAO,IAAK,OAAO,KAC5B,GAAI,EAAK,KAAO,IAAK,CAEnB,IAAI,EAAI,EACR,KAAO,EAAI,EAAK,QAAQ,CACtB,GAAI,EAAK,KAAO,IAAK,MAAO,CAAE,SAAU,EAAG,OAAQ,EAAI,EAAG,CAC1D,IAGF,MAAO,CAAE,SAAU,EAAG,OAAQ,EAAK,OAAQ,CAE7C,IAEF,OAAO,KAaT,SAAS,EACP,EACA,EACA,EACuC,CACvC,IAAI,EAAe,EACf,EAAa,EAEX,EAAW,EAAkB,EAAM,EAAM,CAC3C,IACF,EAAe,EAAS,UAG1B,IAAM,EAAS,EAAkB,EAAM,EAAI,CAQ3C,OAPI,IACF,EAAa,EAAO,QAIlB,GAAgB,EAAmB,KAEhC,CAAE,MAAO,EAAc,IAAK,EAAY,CAiBjD,SAAS,EAAmB,EAAsB,CAChD,IAAM,EAA8B,CAClC,IAAK,QACL,IAAK,OACL,IAAK,OACL,IAAK,SACL,IAAK,QACL,IAAK,SACN,CACD,OAAO,EAAK,QAAQ,YAAc,GAAS,EAAI,GAAM"}
@@ -95,12 +95,21 @@ interface FullCaseCitation extends CitationBase {
95
95
  type: "case";
96
96
  volume: number | string;
97
97
  reporter: string;
98
- page: number;
98
+ /** Page number — optional for blank page placeholder citations (e.g., "___" or "---") */
99
+ page?: number;
99
100
  pincite?: number;
100
101
  court?: string;
101
102
  year?: number;
102
103
  /** Normalized reporter abbreviation from reporters-db (e.g., "F.2d" vs "F. 2d") */
103
104
  normalizedReporter?: string;
105
+ /**
106
+ * Group identifier for parallel citations (same case in multiple reporters).
107
+ * Populated by Phase 8 (Parallel Linking).
108
+ * Format: ${volume}-${reporter}-${page} (e.g., "410-U.S.-113")
109
+ * All citations in the same parallel group share the same groupId.
110
+ * @example "410-U.S.-113" for parallel group [410 U.S. 113, 93 S. Ct. 705]
111
+ */
112
+ groupId?: string;
104
113
  /** Parallel citations for same case in different reporters */
105
114
  parallelCitations?: Array<{
106
115
  volume: number | string;
@@ -137,6 +146,60 @@ interface FullCaseCitation extends CitationBase {
137
146
  confidence: number;
138
147
  reason: string;
139
148
  }>;
149
+ /**
150
+ * Full span covering citation from case name through closing parenthetical.
151
+ * Populated by Phase 6 (Full Span extraction).
152
+ * @example For "Smith v. Doe, 500 F.2d 123 (2020)", fullSpan covers entire text.
153
+ */
154
+ fullSpan?: Span;
155
+ /**
156
+ * Extracted case name (party names around "v.").
157
+ * Populated by Phase 6 (Full Span extraction).
158
+ * @example "Smith v. Doe" or "United States v. Jones"
159
+ */
160
+ caseName?: string;
161
+ /**
162
+ * Plaintiff party name (text before "v." or procedural prefix).
163
+ * Populated by Phase 7 (Party Name extraction).
164
+ * @example "Smith" from "Smith v. Doe" or "Jones" from "In re Jones"
165
+ */
166
+ plaintiff?: string;
167
+ /**
168
+ * Defendant party name (text after "v.").
169
+ * Populated by Phase 7 (Party Name extraction).
170
+ * @example "Doe" from "Smith v. Doe"
171
+ */
172
+ defendant?: string;
173
+ /**
174
+ * Normalized plaintiff name for matching (lowercase, stripped of noise).
175
+ * Populated by Phase 7 (Party Name extraction).
176
+ * @example "smith" from "The Smith Corp., Inc."
177
+ */
178
+ plaintiffNormalized?: string;
179
+ /**
180
+ * Normalized defendant name for matching (lowercase, stripped of noise).
181
+ * Populated by Phase 7 (Party Name extraction).
182
+ * @example "doe" from "Doe et al."
183
+ */
184
+ defendantNormalized?: string;
185
+ /**
186
+ * Procedural prefix for non-adversarial cases.
187
+ * Populated by Phase 7 (Party Name extraction).
188
+ * @example "In re" from "In re Smith"
189
+ */
190
+ proceduralPrefix?: string;
191
+ /**
192
+ * True when page position contains a blank placeholder ("___" or "---").
193
+ * Populated by Phase 5 (Blank Page support).
194
+ * When true, page field will be undefined and confidence reduced to 0.8.
195
+ */
196
+ hasBlankPage?: boolean;
197
+ /**
198
+ * Disposition or procedural status from parenthetical.
199
+ * Populated by Phase 6 (Complex Parentheticals).
200
+ * @example "en banc", "per curiam"
201
+ */
202
+ disposition?: string;
140
203
  }
141
204
  /**
142
205
  * Statute citation (U.S. Code, state codes, etc.).
@@ -323,4 +386,4 @@ type CitationOfType<T extends CitationType> = Extract<Citation, {
323
386
  type ExtractorMap = { [K in FullCitationType]: CitationOfType<K> };
324
387
  //#endregion
325
388
  export { TransformationMap as S, StatuteCitation as _, ExtractorMap as a, Warning as b, FullCitation as c, JournalCitation as d, NeutralCitation as f, ShortFormCitationType as g, ShortFormCitation as h, CitationType as i, FullCitationType as l, ShortFormCaseCitation as m, CitationBase as n, FederalRegisterCitation as o, PublicLawCitation as p, CitationOfType as r, FullCaseCitation as s, Citation as t, IdCitation as u, StatutesAtLargeCitation as v, Span as x, SupraCitation as y };
326
- //# sourceMappingURL=citation-DAyM8kNA.d.mts.map
389
+ //# sourceMappingURL=citation-4bmWbhSK.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citation-4bmWbhSK.d.cts","names":[],"sources":["../src/types/span.ts","../src/types/citation.ts"],"mappings":"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"}
@@ -95,12 +95,21 @@ interface FullCaseCitation extends CitationBase {
95
95
  type: "case";
96
96
  volume: number | string;
97
97
  reporter: string;
98
- page: number;
98
+ /** Page number — optional for blank page placeholder citations (e.g., "___" or "---") */
99
+ page?: number;
99
100
  pincite?: number;
100
101
  court?: string;
101
102
  year?: number;
102
103
  /** Normalized reporter abbreviation from reporters-db (e.g., "F.2d" vs "F. 2d") */
103
104
  normalizedReporter?: string;
105
+ /**
106
+ * Group identifier for parallel citations (same case in multiple reporters).
107
+ * Populated by Phase 8 (Parallel Linking).
108
+ * Format: ${volume}-${reporter}-${page} (e.g., "410-U.S.-113")
109
+ * All citations in the same parallel group share the same groupId.
110
+ * @example "410-U.S.-113" for parallel group [410 U.S. 113, 93 S. Ct. 705]
111
+ */
112
+ groupId?: string;
104
113
  /** Parallel citations for same case in different reporters */
105
114
  parallelCitations?: Array<{
106
115
  volume: number | string;
@@ -137,6 +146,60 @@ interface FullCaseCitation extends CitationBase {
137
146
  confidence: number;
138
147
  reason: string;
139
148
  }>;
149
+ /**
150
+ * Full span covering citation from case name through closing parenthetical.
151
+ * Populated by Phase 6 (Full Span extraction).
152
+ * @example For "Smith v. Doe, 500 F.2d 123 (2020)", fullSpan covers entire text.
153
+ */
154
+ fullSpan?: Span;
155
+ /**
156
+ * Extracted case name (party names around "v.").
157
+ * Populated by Phase 6 (Full Span extraction).
158
+ * @example "Smith v. Doe" or "United States v. Jones"
159
+ */
160
+ caseName?: string;
161
+ /**
162
+ * Plaintiff party name (text before "v." or procedural prefix).
163
+ * Populated by Phase 7 (Party Name extraction).
164
+ * @example "Smith" from "Smith v. Doe" or "Jones" from "In re Jones"
165
+ */
166
+ plaintiff?: string;
167
+ /**
168
+ * Defendant party name (text after "v.").
169
+ * Populated by Phase 7 (Party Name extraction).
170
+ * @example "Doe" from "Smith v. Doe"
171
+ */
172
+ defendant?: string;
173
+ /**
174
+ * Normalized plaintiff name for matching (lowercase, stripped of noise).
175
+ * Populated by Phase 7 (Party Name extraction).
176
+ * @example "smith" from "The Smith Corp., Inc."
177
+ */
178
+ plaintiffNormalized?: string;
179
+ /**
180
+ * Normalized defendant name for matching (lowercase, stripped of noise).
181
+ * Populated by Phase 7 (Party Name extraction).
182
+ * @example "doe" from "Doe et al."
183
+ */
184
+ defendantNormalized?: string;
185
+ /**
186
+ * Procedural prefix for non-adversarial cases.
187
+ * Populated by Phase 7 (Party Name extraction).
188
+ * @example "In re" from "In re Smith"
189
+ */
190
+ proceduralPrefix?: string;
191
+ /**
192
+ * True when page position contains a blank placeholder ("___" or "---").
193
+ * Populated by Phase 5 (Blank Page support).
194
+ * When true, page field will be undefined and confidence reduced to 0.8.
195
+ */
196
+ hasBlankPage?: boolean;
197
+ /**
198
+ * Disposition or procedural status from parenthetical.
199
+ * Populated by Phase 6 (Complex Parentheticals).
200
+ * @example "en banc", "per curiam"
201
+ */
202
+ disposition?: string;
140
203
  }
141
204
  /**
142
205
  * Statute citation (U.S. Code, state codes, etc.).
@@ -323,4 +386,4 @@ type CitationOfType<T extends CitationType> = Extract<Citation, {
323
386
  type ExtractorMap = { [K in FullCitationType]: CitationOfType<K> };
324
387
  //#endregion
325
388
  export { TransformationMap as S, StatuteCitation as _, ExtractorMap as a, Warning as b, FullCitation as c, JournalCitation as d, NeutralCitation as f, ShortFormCitationType as g, ShortFormCitation as h, CitationType as i, FullCitationType as l, ShortFormCaseCitation as m, CitationBase as n, FederalRegisterCitation as o, PublicLawCitation as p, CitationOfType as r, FullCaseCitation as s, Citation as t, IdCitation as u, StatutesAtLargeCitation as v, Span as x, SupraCitation as y };
326
- //# sourceMappingURL=citation-qKSc_Myj.d.cts.map
389
+ //# sourceMappingURL=citation-BVN0o8TJ.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citation-BVN0o8TJ.d.mts","names":[],"sources":["../src/types/span.ts","../src/types/citation.ts"],"mappings":"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"}