eyecite-ts 0.2.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
@@ -1,18 +1,30 @@
1
1
  # eyecite-ts
2
2
 
3
- TypeScript legal citation extraction library — port of Python [eyecite](https://github.com/freelawproject/eyecite).
3
+ [![CI](https://github.com/medelman17/eyecite-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/medelman17/eyecite-ts/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/medelman17/eyecite-ts/branch/main/graph/badge.svg)](https://codecov.io/gh/medelman17/eyecite-ts)
5
+ [![npm version](https://img.shields.io/npm/v/eyecite-ts.svg)](https://www.npmjs.com/package/eyecite-ts)
6
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/eyecite-ts)](https://bundlephobia.com/package/eyecite-ts)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+ [![Node.js](https://img.shields.io/node/v/eyecite-ts.svg)](https://nodejs.org)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
10
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](https://www.npmjs.com/package/eyecite-ts)
11
+
12
+ TypeScript legal citation extraction library — inspired by and extending Python [eyecite](https://github.com/freelawproject/eyecite).
4
13
 
5
14
  Extract, resolve, and annotate legal citations from court opinions and legal documents with zero runtime dependencies.
6
15
 
7
16
  ## Features
8
17
 
9
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
10
22
  - **Short-form resolution**: Id./Ibid., supra, and short-form case citations resolved to their full antecedents
11
23
  - **Reporter database**: 1,200+ reporters with variant matching and confidence scoring
12
24
  - **Citation annotation**: HTML markup with auto-escape XSS protection and position tracking
13
25
  - **Bundle optimization**: Tree-shakeable exports, lazy-loaded reporter data, separate entry points
14
26
  - **TypeScript native**: Discriminated unions, conditional types, type guards, full IntelliSense
15
- - **Zero dependencies**: No runtime dependencies, 4.4KB gzipped core bundle
27
+ - **Zero dependencies**: No runtime dependencies, 7KB gzipped core bundle
16
28
 
17
29
  ## Installation
18
30
 
@@ -25,7 +37,7 @@ npm install eyecite-ts
25
37
  ```typescript
26
38
  import { extractCitations } from 'eyecite-ts'
27
39
 
28
- 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)'
29
41
  const citations = extractCitations(text)
30
42
 
31
43
  console.log(citations[0])
@@ -36,8 +48,11 @@ console.log(citations[0])
36
48
  // page: 123,
37
49
  // court: '9th Cir.',
38
50
  // year: 2020,
51
+ // caseName: 'Smith v. Jones',
52
+ // date: { iso: '2020-01-15', parsed: { year: 2020, month: 1, day: 15 } },
39
53
  // confidence: 0.85,
40
- // span: { originalStart: 4, originalEnd: 48, cleanStart: 4, cleanEnd: 48 }
54
+ // span: { originalStart: 20, originalEnd: 33, ... },
55
+ // fullSpan: { originalStart: 4, originalEnd: 57, ... }
41
56
  // }
42
57
  ```
43
58
 
@@ -91,6 +106,97 @@ const citations = extractCitations(html, {
91
106
  })
92
107
  ```
93
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
+
94
200
  ## Resolving Short-Form Citations
95
201
 
96
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.
@@ -212,6 +318,38 @@ const result = annotate(text, citations, {
212
318
  })
213
319
  ```
214
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
+
215
353
  ## Reporter Validation
216
354
 
217
355
  Validate case citations against the reporters database:
@@ -296,9 +434,9 @@ Three entry points for optimal tree-shaking:
296
434
 
297
435
  | Entry Point | Import | Gzipped |
298
436
  |------------|--------|---------|
299
- | Core extraction | `eyecite-ts` | 4.4 KB |
300
- | Annotation | `eyecite-ts/annotate` | 0.5 KB |
301
- | 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) |
302
440
 
303
441
  ```typescript
304
442
  import { extractCitations } from 'eyecite-ts' // Core only
@@ -322,16 +460,16 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for details.
322
460
  ## Development
323
461
 
324
462
  ```bash
325
- npm install # Install dependencies
326
- npm test # Run tests (vitest, watch mode)
327
- npx vitest run # Run tests once
328
- npm run typecheck # Type-check with tsc
329
- npm run build # Build (ESM + CJS + DTS)
330
- npm run lint # Lint with Biome
331
- npm run format # Format with Biome
463
+ pnpm install # Install dependencies
464
+ pnpm test # Run tests (vitest, watch mode)
465
+ pnpm exec vitest run # Run tests once
466
+ pnpm typecheck # Type-check with tsc
467
+ pnpm build # Build (ESM + CJS + DTS)
468
+ pnpm lint # Lint with Biome
469
+ pnpm format # Format with Biome
332
470
  ```
333
471
 
334
- 304 tests, 97% statement coverage, 91% branch coverage.
472
+ 527 tests across 22 test files.
335
473
 
336
474
  ## License
337
475
 
@@ -339,4 +477,4 @@ MIT
339
477
 
340
478
  ## Credits
341
479
 
342
- 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,n,r={}){let{useCleanText:i=!1,autoEscape:a=!0,template:o,callback:s}=r,c=[...n].sort((e,t)=>{let n=i?e.span.cleanStart:e.span.originalStart;return(i?t.span.cleanStart:t.span.originalStart)-n}),l=e,u=new Map;for(let n of c){let r=i?n.span.cleanStart:n.span.originalStart,c=i?n.span.cleanEnd:n.span.originalEnd,d=``;if(s)d=s(n,e.substring(Math.max(0,r-30),Math.min(e.length,c+30)));else if(o){let e=l.substring(r,c),n=a?t(e):e;d=o.before+n+o.after}else continue;l=l.slice(0,r)+d+l.slice(c),u.set(r,r)}return{text:l,positionMap:u,skipped:[]}}function t(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\n for (const citation of sorted) {\n const start = useCleanText ? citation.span.cleanStart : citation.span.originalStart\n const end = useCleanText ? citation.span.cleanEnd : citation.span.originalEnd\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 * 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,IAExB,IAAK,IAAM,KAAY,EAAQ,CAC7B,IAAM,EAAQ,EAAe,EAAS,KAAK,WAAa,EAAS,KAAK,cAChE,EAAM,EAAe,EAAS,KAAK,SAAW,EAAS,KAAK,YAE9D,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,QAAS,EAAE,CAAE,CAiBnD,SAAS,EAAmB,EAAsB,CAChD,IAAM,EAA8B,CAClC,IAAK,QACL,IAAK,OACL,IAAK,OACL,IAAK,SACL,IAAK,QACL,IAAK,SACN,CACD,OAAO,EAAK,QAAQ,aAAe,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-BhJJj_AZ.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-FJ10UFM7.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,n,r={}){let{useCleanText:i=!1,autoEscape:a=!0,template:o,callback:s}=r,c=[...n].sort((e,t)=>{let n=i?e.span.cleanStart:e.span.originalStart;return(i?t.span.cleanStart:t.span.originalStart)-n}),l=e,u=new Map;for(let n of c){let r=i?n.span.cleanStart:n.span.originalStart,c=i?n.span.cleanEnd:n.span.originalEnd,d=``;if(s)d=s(n,e.substring(Math.max(0,r-30),Math.min(e.length,c+30)));else if(o){let e=l.substring(r,c),n=a?t(e):e;d=o.before+n+o.after}else continue;l=l.slice(0,r)+d+l.slice(c),u.set(r,r)}return{text:l,positionMap:u,skipped:[]}}function t(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\n for (const citation of sorted) {\n const start = useCleanText ? citation.span.cleanStart : citation.span.originalStart\n const end = useCleanText ? citation.span.cleanEnd : citation.span.originalEnd\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 * 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,IAExB,IAAK,IAAM,KAAY,EAAQ,CAC7B,IAAM,EAAQ,EAAe,EAAS,KAAK,WAAa,EAAS,KAAK,cAChE,EAAM,EAAe,EAAS,KAAK,SAAW,EAAS,KAAK,YAE9D,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,QAAS,EAAE,CAAE,CAiBnD,SAAS,EAAmB,EAAsB,CAChD,IAAM,EAA8B,CAClC,IAAK,QACL,IAAK,OACL,IAAK,OACL,IAAK,SACL,IAAK,QACL,IAAK,SACN,CACD,OAAO,EAAK,QAAQ,aAAe,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"}
@@ -43,7 +43,7 @@ interface TransformationMap {
43
43
  /**
44
44
  * Citation type discriminator for type-safe pattern matching.
45
45
  */
46
- type CitationType = "case" | "statute" | "journal" | "neutral" | "publicLaw" | "federalRegister" | "id" | "supra" | "shortFormCase";
46
+ type CitationType = "case" | "statute" | "journal" | "neutral" | "publicLaw" | "federalRegister" | "statutesAtLarge" | "id" | "supra" | "shortFormCase";
47
47
  /**
48
48
  * Warning generated during citation parsing.
49
49
  */
@@ -93,17 +93,26 @@ interface CitationBase {
93
93
  */
94
94
  interface FullCaseCitation extends CitationBase {
95
95
  type: "case";
96
- volume: number;
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
- volume: number;
115
+ volume: number | string;
107
116
  reporter: string;
108
117
  page: number;
109
118
  }>;
@@ -131,12 +140,66 @@ interface FullCaseCitation extends CitationBase {
131
140
  * Used when reporter abbreviation matches multiple reporters or format is unclear.
132
141
  */
133
142
  possibleInterpretations?: Array<{
134
- volume: number;
143
+ volume: number | string;
135
144
  reporter: string;
136
145
  page: number;
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.).
@@ -163,8 +226,8 @@ interface JournalCitation extends CitationBase {
163
226
  author?: string;
164
227
  /** Article title (if extracted) */
165
228
  title?: string;
166
- /** Volume number */
167
- volume?: number;
229
+ /** Volume number (string for hyphenated volumes like "1984-1") */
230
+ volume?: number | string;
168
231
  /** Full journal name */
169
232
  journal: string;
170
233
  /** Standard journal abbreviation (e.g., "Harv. L. Rev.") */
@@ -221,7 +284,17 @@ interface PublicLawCitation extends CitationBase {
221
284
  interface FederalRegisterCitation extends CitationBase {
222
285
  type: "federalRegister";
223
286
  /** Federal Register volume */
224
- volume: number;
287
+ volume: number | string;
288
+ /** Page number */
289
+ page: number;
290
+ /** Publication year (if extracted) */
291
+ year?: number;
292
+ }
293
+ /** Citation to the Statutes at Large (session law compilation) */
294
+ interface StatutesAtLargeCitation extends CitationBase {
295
+ type: "statutesAtLarge";
296
+ /** Statutes at Large volume */
297
+ volume: number | string;
225
298
  /** Page number */
226
299
  page: number;
227
300
  /** Publication year (if extracted) */
@@ -258,7 +331,7 @@ interface SupraCitation extends CitationBase {
258
331
  */
259
332
  interface ShortFormCaseCitation extends CitationBase {
260
333
  type: "shortFormCase";
261
- volume: number;
334
+ volume: number | string;
262
335
  reporter: string;
263
336
  page?: number;
264
337
  pincite?: number;
@@ -280,16 +353,16 @@ interface ShortFormCaseCitation extends CitationBase {
280
353
  * // ...
281
354
  * }
282
355
  */
283
- type Citation = FullCaseCitation | StatuteCitation | JournalCitation | NeutralCitation | PublicLawCitation | FederalRegisterCitation | IdCitation | SupraCitation | ShortFormCaseCitation;
356
+ type Citation = FullCaseCitation | StatuteCitation | JournalCitation | NeutralCitation | PublicLawCitation | FederalRegisterCitation | StatutesAtLargeCitation | IdCitation | SupraCitation | ShortFormCaseCitation;
284
357
  /**
285
358
  * Citation type discriminators grouped by category.
286
359
  */
287
- type FullCitationType = "case" | "statute" | "journal" | "neutral" | "publicLaw" | "federalRegister";
360
+ type FullCitationType = "case" | "statute" | "journal" | "neutral" | "publicLaw" | "federalRegister" | "statutesAtLarge";
288
361
  type ShortFormCitationType = "id" | "supra" | "shortFormCase";
289
362
  /**
290
363
  * Union of all full citation types (not short-form references).
291
364
  */
292
- type FullCitation = FullCaseCitation | StatuteCitation | JournalCitation | NeutralCitation | PublicLawCitation | FederalRegisterCitation;
365
+ type FullCitation = FullCaseCitation | StatuteCitation | JournalCitation | NeutralCitation | PublicLawCitation | FederalRegisterCitation | StatutesAtLargeCitation;
293
366
  /**
294
367
  * Union of all short-form citation types (Id., supra, short-form case).
295
368
  */
@@ -312,5 +385,5 @@ type CitationOfType<T extends CitationType> = Extract<Citation, {
312
385
  */
313
386
  type ExtractorMap = { [K in FullCitationType]: CitationOfType<K> };
314
387
  //#endregion
315
- export { StatuteCitation as _, ExtractorMap as a, Span 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, SupraCitation as v, TransformationMap as x, Warning as y };
316
- //# sourceMappingURL=citation-BhJJj_AZ.d.cts.map
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 };
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":";;AAiBA;;;;;;;;;;AAoBA;;;;;;UApBiB,IAAA;;EAEf,UAAA;EAuBiB;EApBjB,QAAA;;EAGA,aAAA;ECpBF;EDuBE,WAAA;AAAA;;;AClBF;;;;UD2BiB,iBAAA;;EAEf,eAAA,EAAiB,GAAA;;EAGjB,eAAA,EAAiB,GAAA;AAAA;;;AAzBnB;;;AAAA,KCZY,YAAA;;;;UAKK,OAAA;;EAEf,KAAA;EDyBF;ECvBE,OAAA;;EAEA,QAAA;IAAY,KAAA;IAAe,GAAA;EAAA;;EAE3B,OAAA;AAAA;;;;UAMe,YAAA;EAnBL;EAqBV,IAAA;EArBU;EAwBV,IAAA,EAAM,IAAA;EAnBR;;;;;;;EA4BE,UAAA;;EAGA,WAAA;;EAGA,aAAA;EApBF;EAuBE,eAAA;;EAGA,QAAA,GAAW,OAAA;AAAA;;;;;;;UASI,gBAAA,SAAyB,YAAA;EACxC,IAAA;EACA,MAAA;EACA,QAAA;EAHF;EAKE,IAAA;EACA,OAAA;EACA,KAAA;EACA,IAAA;;EAGA,kBAAA;;;;;;;;EASA,OAAA;;EAGA,iBAAA,GAAoB,KAAA;IAClB,MAAA;IACA,QAAA;IACA,IAAA;EAAA;;EAIF,MAAA;;EAGA,aAAA;;EAGA,iBAAA;;;;;;EAOA,IAAA;IACE,GAAA;IACA,MAAA;MAAW,IAAA;MAAc,KAAA;MAAgB,GAAA;IAAA;EAAA;;;;;EAO3C,uBAAA,GAA0B,KAAA;IACxB,MAAA;IACA,QAAA;IACA,IAAA;IACA,UAAA;IACA,MAAA;EAAA;EAgEF;AAQF;;;;EAhEE,QAAA,GAAW,IAAA;;;;;;EAOX,QAAA;EAwEF;;;;;EAjEE,SAAA;;;;;;EAOA,SAAA;;;;;AAsFF;EA/EE,mBAAA;;;;;;EAOA,mBAAA;;;;AA0FF;;EAnFE,gBAAA;EAmFyC;;;;;EA5EzC,YAAA;;;AA8FF;;;EAvFE,WAAA;AAAA;;;;;;UAQe,eAAA,SAAwB,YAAA;EACvC,IAAA;EACA,KAAA;EACA,IAAA;EACA,OAAA;AAAA;;;;;;;AAsGF;;UA3FiB,eAAA,SAAwB,YAAA;EACvC,IAAA;;EAEA,MAAA;;EAEA,KAAA;EAwFA;EAtFA,MAAA;EA+Fe;EA7Ff,OAAA;EA6FqC;EA3FrC,YAAA;;EAEA,IAAA;;EAEA,OAAA;EA4FA;EA1FA,IAAA;AAAA;;;;;;;;;UAWe,eAAA,SAAwB,YAAA;EACvC,IAAA;EAgHF;EA9GE,IAAA;;EAEA,KAAA;;EAEA,cAAA;AAAA;;;;;;;;;UAWe,iBAAA,SAA0B,YAAA;EACzC,IAAA;;EAEA,QAAA;;EAEA,SAAA;;EAEA,KAAA;AAAA;;;;AAuGF;;;;;UA5FiB,uBAAA,SAAgC,YAAA;EAC/C,IAAA;;EAEA,MAAA;EA0FU;EAxFV,IAAA;EA6FU;EA3FV,IAAA;AAAA;;UAIe,uBAAA,SAAgC,YAAA;EAC/C,IAAA;;EAEA,MAAA;;EAEA,IAAA;EAkFgJ;EAhFhJ,IAAA;AAAA;;;;;;;UASe,UAAA,SAAmB,YAAA;EAClC,IAAA;EACA,OAAA;AAAA;;;;;;;UASe,aAAA,SAAsB,YAAA;EACrC,IAAA;;EAEA,SAAA;EAyEF;EAvEE,OAAA;AAAA;;;;;;;UASe,qBAAA,SAA8B,YAAA;EAC7C,IAAA;EACA,MAAA;EACA,QAAA;EACA,IAAA;EACA,OAAA;AAAA;;AA+DF;;;;;;;;;;;;;;;;KA3CY,QAAA,GACR,gBAAA,GACA,eAAA,GACA,eAAA,GACA,eAAA,GACA,iBAAA,GACA,uBAAA,GACA,uBAAA,GACA,UAAA,GACA,aAAA,GACA,qBAAA;;;;KAKQ,gBAAA;AAAA,KACA,qBAAA;;;;KAKA,YAAA,GAAe,gBAAA,GAAmB,eAAA,GAAkB,eAAA,GAAkB,eAAA,GAAkB,iBAAA,GAAoB,uBAAA,GAA0B,uBAAA;;;;KAKtI,iBAAA,GAAoB,UAAA,GAAa,aAAA,GAAgB,qBAAA;;;;;;;;;;KAWjD,cAAA,WAAyB,YAAA,IAAgB,OAAA,CAAQ,QAAA;EAAY,IAAA,EAAM,CAAA;AAAA;;;;;KAMnE,YAAA,WACJ,gBAAA,GAAmB,cAAA,CAAe,CAAA"}