{"version":3,"sources":["../src/index.ts","../src/repair.ts","../src/scan.ts","../src/strip.ts","../src/types.ts","../src/extract.ts"],"sourcesContent":["export { extractJson, tryExtractJson } from './extract.ts';\nexport { stripReasoning, fencedBlocks } from './strip.ts';\nexport { balancedSpans } from './scan.ts';\nexport { removeTrailingCommas } from './repair.ts';\nexport { JsonExtractionError } from './types.ts';\nexport type { ExtractOptions, ExtractResult } from './types.ts';\n","/**\n * Remove trailing commas (`{\"a\":1,}` → `{\"a\":1}`, `[1,2,]` → `[1,2]`), which\n * models emit frequently. String-aware: a comma inside a string value is never\n * touched, so this can only ever fix structure, never corrupt content.\n */\nexport function removeTrailingCommas(json: string): string {\n  let out = '';\n  let inString = false;\n  let escaped = false;\n\n  for (let i = 0; i < json.length; i++) {\n    const ch = json[i];\n\n    if (inString) {\n      out += ch;\n      if (escaped) {\n        escaped = false;\n      } else if (ch === '\\\\') {\n        escaped = true;\n      } else if (ch === '\"') {\n        inString = false;\n      }\n      continue;\n    }\n\n    if (ch === '\"') {\n      inString = true;\n      out += ch;\n      continue;\n    }\n\n    if (ch === ',') {\n      let j = i + 1;\n      while (\n        j < json.length &&\n        (json[j] === ' ' ||\n          json[j] === '\\n' ||\n          json[j] === '\\r' ||\n          json[j] === '\\t')\n      ) {\n        j++;\n      }\n      if (json[j] === '}' || json[j] === ']') {\n        continue; // drop the trailing comma\n      }\n    }\n\n    out += ch;\n  }\n\n  return out;\n}\n","/**\n * Find the substrings of complete, balanced JSON objects/arrays in `text`,\n * in document order. String-aware and delimiter-aware: braces and brackets\n * inside JSON strings do not affect nesting, and `[` must close with `]`.\n */\nexport function balancedSpans(text: string): string[] {\n  const spans: string[] = [];\n  let i = 0;\n  while (i < text.length) {\n    const ch = text[i];\n    if (ch === '{' || ch === '[') {\n      const match = matchBalanced(text, i);\n      if (match.end !== -1) {\n        spans.push(text.slice(i, match.end));\n        i = match.end;\n        continue;\n      }\n\n      i = Math.max(match.resume, i + 1);\n      continue;\n    }\n    i++;\n  }\n  return spans;\n}\n\ninterface MatchResult {\n  /** Index just past the balanced value, or -1 when no complete value exists. */\n  end: number;\n  /** Next scan index after a malformed or incomplete candidate. */\n  resume: number;\n}\n\nfunction matchBalanced(text: string, start: number): MatchResult {\n  const expectedClosers: string[] = [];\n  let inString = false;\n  let escaped = false;\n  let sawCloserOrQuote = false;\n\n  for (let i = start; i < text.length; i++) {\n    const ch = text[i];\n\n    if (inString) {\n      if (escaped) {\n        escaped = false;\n      } else if (ch === '\\\\') {\n        escaped = true;\n      } else if (ch === '\"') {\n        inString = false;\n      }\n      continue;\n    }\n\n    if (ch === '\"') {\n      inString = true;\n      sawCloserOrQuote = true;\n      continue;\n    }\n\n    if (ch === '{') {\n      expectedClosers.push('}');\n      continue;\n    }\n\n    if (ch === '[') {\n      expectedClosers.push(']');\n      continue;\n    }\n\n    if (ch === '}' || ch === ']') {\n      sawCloserOrQuote = true;\n      if (expectedClosers.pop() !== ch) {\n        return { end: -1, resume: i + 1 };\n      }\n      if (expectedClosers.length === 0) {\n        return { end: i + 1, resume: i + 1 };\n      }\n    }\n  }\n\n  // A balanced span needs a closer, and string state cannot diverge without a\n  // quote — so when the rest of the text has neither, no later start can\n  // succeed. This keeps degenerate runs of openers (a model stuck repeating\n  // `{`) linear instead of rescanning the tail from every position.\n  return {\n    end: -1,\n    resume:\n      !sawCloserOrQuote || looksLikeJsonContainerStart(text, start)\n        ? text.length\n        : start + 1,\n  };\n}\n\nfunction looksLikeJsonContainerStart(text: string, start: number): boolean {\n  let index = start + 1;\n  while (index < text.length && /\\s/.test(text[index])) {\n    index++;\n  }\n\n  const next = text[index];\n  if (text[start] === '{') {\n    return next === '\"' || next === '}';\n  }\n\n  return (\n    next === undefined ||\n    next === '[' ||\n    next === '{' ||\n    next === '\"' ||\n    next === ']' ||\n    next === '-' ||\n    (next >= '0' && next <= '9') ||\n    next === 't' ||\n    next === 'f' ||\n    next === 'n'\n  );\n}\n","/**\n * Remove model \"thinking\" / reasoning blocks. Reasoning models (DeepSeek R1,\n * Qwen, and prompted Claude/Gemini setups) emit `<think>…</think>` or\n * `<thinking>…</thinking>` before the answer, and that text frequently contains\n * brace-laden prose that would otherwise be mistaken for the payload.\n *\n * If a reasoning tag is opened but not closed, treat the rest of the text as\n * reasoning. Returning no JSON is safer than extracting a valid-looking draft.\n */\nconst CLOSED_REASONING_BLOCK =\n  /<(think|thinking|reasoning|thought)\\b[^>]*>[\\s\\S]*?<\\/\\1>/gi;\nconst OPEN_REASONING_TAG = /<(think|thinking|reasoning|thought)\\b[^>]*>/gi;\n\nexport function stripReasoning(text: string): string {\n  const withoutClosedBlocks = text.replace(CLOSED_REASONING_BLOCK, '');\n  OPEN_REASONING_TAG.lastIndex = 0;\n  const unclosed = OPEN_REASONING_TAG.exec(withoutClosedBlocks);\n\n  if (!unclosed) {\n    return withoutClosedBlocks;\n  }\n\n  return withoutClosedBlocks.slice(0, unclosed.index);\n}\n\n/**\n * Return the inner contents of fenced code blocks that could hold JSON: blocks\n * tagged ```json / ```jsonc / ```json5, or untagged ``` blocks. Other languages\n * (```python, ```ts) are skipped — they won't contain the answer JSON.\n */\nconst FENCE = /```[^\\S\\n]*([a-zA-Z0-9_+-]*)[^\\S\\n]*\\n?([\\s\\S]*?)```/g;\n\nexport function fencedBlocks(text: string): string[] {\n  const blocks: string[] = [];\n  FENCE.lastIndex = 0;\n  let match: RegExpExecArray | null;\n  while ((match = FENCE.exec(text)) !== null) {\n    const lang = match[1].toLowerCase();\n    const content = match[2].trim();\n    if (content.length > 0 && (lang === '' || lang.includes('json'))) {\n      blocks.push(content);\n    }\n  }\n  return blocks;\n}\n","/** Options for {@link extractJson} and {@link tryExtractJson}. */\nexport interface ExtractOptions {\n  /**\n   * Apply conservative, string-aware repairs before parsing — currently the\n   * removal of trailing commas, which models emit often. Never rewrites string\n   * contents. Default `true`.\n   */\n  repair?: boolean;\n  /**\n   * Restrict which top-level JSON value to accept: an `'object'`, an `'array'`,\n   * or `'any'` (the default).\n   */\n  expect?: 'object' | 'array' | 'any';\n}\n\n/** The result of {@link tryExtractJson}. */\nexport type ExtractResult<T> =\n  { found: true; value: T } | { found: false; value?: undefined };\n\n/** Thrown by {@link extractJson} when no JSON value can be recovered. */\nexport class JsonExtractionError extends Error {\n  constructor(\n    message: string,\n    /** The original text that no JSON could be extracted from. */\n    public readonly text: string,\n  ) {\n    super(message);\n    this.name = 'JsonExtractionError';\n  }\n}\n","import { removeTrailingCommas } from './repair.ts';\nimport { balancedSpans } from './scan.ts';\nimport { fencedBlocks, stripReasoning } from './strip.ts';\nimport { JsonExtractionError } from './types.ts';\nimport type { ExtractOptions, ExtractResult } from './types.ts';\n\nfunction parseCandidate(\n  candidate: string,\n  repair: boolean,\n): { ok: true; value: unknown } | { ok: false } {\n  try {\n    return { ok: true, value: JSON.parse(candidate) };\n  } catch {\n    // fall through to repair\n  }\n  if (repair) {\n    try {\n      return { ok: true, value: JSON.parse(removeTrailingCommas(candidate)) };\n    } catch {\n      // unrecoverable\n    }\n  }\n  return { ok: false };\n}\n\nfunction matchesExpect(\n  value: unknown,\n  expect: 'object' | 'array' | 'any',\n): boolean {\n  if (expect === 'any') {\n    return true;\n  }\n  if (expect === 'array') {\n    return Array.isArray(value);\n  }\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Extract a JSON value from LLM output without throwing.\n *\n * Strips `<think>` / `<thinking>` reasoning blocks, prefers fenced ```json\n * code blocks, then scans for the first balanced object/array that parses\n * (applying conservative repair). Returns `{ found: false }` if nothing parses.\n *\n * @example\n * ```ts\n * const r = tryExtractJson<{ score: number }>('<think>...</think>\\n{\"score\":7}');\n * if (r.found) console.log(r.value.score); // 7\n * ```\n */\nexport function tryExtractJson<T = unknown>(\n  text: string,\n  options: ExtractOptions = {},\n): ExtractResult<T> {\n  if (typeof text !== 'string' || text.length === 0) {\n    return { found: false };\n  }\n\n  const repair = options.repair ?? true;\n  const expect = options.expect ?? 'any';\n  const cleaned = stripReasoning(text);\n\n  // Candidate substrings, highest confidence first: fenced blocks (and any\n  // balanced values inside them), then balanced values anywhere in the text.\n  const candidates: string[] = [];\n  const blocks = fencedBlocks(cleaned);\n  candidates.push(...blocks);\n  for (const block of blocks) {\n    candidates.push(...balancedSpans(block));\n  }\n  candidates.push(...balancedSpans(cleaned));\n\n  for (const candidate of candidates) {\n    const parsed = parseCandidate(candidate, repair);\n    if (parsed.ok && matchesExpect(parsed.value, expect)) {\n      return { found: true, value: parsed.value as T };\n    }\n  }\n  return { found: false };\n}\n\n/**\n * Extract a JSON value from LLM output, throwing {@link JsonExtractionError}\n * if none can be recovered. See {@link tryExtractJson} for the algorithm.\n */\nexport function extractJson<T = unknown>(\n  text: string,\n  options: ExtractOptions = {},\n): T {\n  const result = tryExtractJson<T>(text, options);\n  if (!result.found) {\n    throw new JsonExtractionError(\n      'No JSON value could be extracted from the text.',\n      text,\n    );\n  }\n  return result.value;\n}\n"],"mappings":"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","names":[]}