// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { EXTRACT_REGEXP_MAP, RECOGNIZE_REGEXP_MAP } from "./_formats.ts";
import type { Format } from "./_types.ts";

/** Return type for {@linkcode Extractor}. */
export type Extract<T> = {
  frontMatter: string;
  body: string;
  attrs: T;
};

/** Function return type for {@linkcode createExtractor}. */
export type Extractor = <T = Record<string, unknown>>(
  str: string,
) => Extract<T>;

/** Parser function type used alongside {@linkcode createExtractor}. */
export type Parser = <T = Record<string, unknown>>(str: string) => T;

function _extract<T>(
  str: string,
  rx: RegExp,
  parse: Parser,
): Extract<T> {
  const match = rx.exec(str);
  if (!match || match.index !== 0) {
    throw new TypeError("Unexpected end of input");
  }
  const frontMatter = match.at(-1)?.replace(/^\s+|\s+$/g, "") || "";
  const attrs = parse(frontMatter) as T;
  const body = str.replace(match[0], "");
  return { frontMatter, body, attrs };
}

/**
 * Recognizes the format of the front matter in a string.
 * Supports {@link https://yaml.org | YAML}, {@link https://toml.io | TOML} and
 * {@link https://www.json.org/ | JSON}.
 *
 * @param str String to recognize.
 * @param formats A list of formats to recognize. Defaults to all supported formats.
 */
function recognize(str: string, formats?: Format[]): Format {
  if (!formats) {
    formats = Object.keys(RECOGNIZE_REGEXP_MAP) as Format[];
  }

  const [firstLine] = str.split(/(\r?\n)/) as [string];

  for (const format of formats) {
    if (format === "unknown") {
      continue;
    }

    if (RECOGNIZE_REGEXP_MAP[format].test(firstLine)) {
      return format;
    }
  }

  return "unknown";
}

/**
 * Factory that creates a function that extracts front matter from a string with
 * the given parsers. Supports {@link https://yaml.org | YAML},
 * {@link https://toml.io | TOML} and {@link https://www.json.org/ | JSON}.
 *
 * For simple use cases where you know which format to parse in advance, use the
 * pre-built extractors:
 *
 * - {@linkcode https://jsr.io/@std/front-matter/doc/yaml/~/extract | extractYaml}
 * - {@linkcode https://jsr.io/@std/front-matter/doc/toml/~/extract | extractToml}
 * - {@linkcode https://jsr.io/@std/front-matter/doc/json/~/extract | extractJson}
 *
 * @param formats A descriptor containing Format-parser pairs to use for each format.
 * @returns A function that extracts front matter from a string with the given parsers.
 *
 * @example Extract YAML front matter
 * ```ts
 * import { createExtractor, Parser } from "@std/front-matter";
 * import { assertEquals } from "@std/assert";
 * import { parse as parseYaml } from "@std/yaml/parse";
 *
 * const extractYaml = createExtractor({ yaml: parseYaml as Parser });
 * const { attrs, body, frontMatter } = extractYaml<{ title: string }>(
 * `---
 * title: Three dashes marks the spot
 * ---
 * ferret`);
 * assertEquals(attrs.title, "Three dashes marks the spot");
 * assertEquals(body, "ferret");
 * assertEquals(frontMatter, "title: Three dashes marks the spot");
 * ```
 *
 * @example Extract TOML front matter
 * ```ts
 * import { createExtractor, Parser } from "@std/front-matter";
 * import { assertEquals } from "@std/assert";
 * import { parse as parseToml } from "@std/toml/parse";
 *
 * const extractToml = createExtractor({ toml: parseToml as Parser });
 * const { attrs, body, frontMatter } = extractToml<{ title: string }>(
 * `---toml
 * title = 'Three dashes followed by format marks the spot'
 * ---
 * `);
 * assertEquals(attrs.title, "Three dashes followed by format marks the spot");
 * assertEquals(body, "");
 * assertEquals(frontMatter, "title = 'Three dashes followed by format marks the spot'");
 * ```
 *
 * @example Extract JSON front matter
 * ```ts
 * import { createExtractor, Parser } from "@std/front-matter";
 * import { assertEquals } from "@std/assert";
 *
 * const extractJson = createExtractor({ json: JSON.parse as Parser });
 * const { attrs, body, frontMatter } = extractJson<{ title: string }>(
 * `---json
 * {"title": "Three dashes followed by format marks the spot"}
 * ---
 * goat`);
 * assertEquals(attrs.title, "Three dashes followed by format marks the spot");
 * assertEquals(body, "goat");
 * assertEquals(frontMatter, `{"title": "Three dashes followed by format marks the spot"}`);
 * ```
 *
 * @example Extract YAML or JSON front matter
 * ```ts
 * import { createExtractor, Parser } from "@std/front-matter";
 * import { assertEquals } from "@std/assert";
 * import { parse as parseYaml } from "@std/yaml/parse";
 *
 * const extractYamlOrJson = createExtractor({
 *   yaml: parseYaml as Parser,
 *   json: JSON.parse as Parser,
 * });
 *
 * let { attrs, body, frontMatter } = extractYamlOrJson<{ title: string }>(
 * `---
 * title: Three dashes marks the spot
 * ---
 * ferret`);
 * assertEquals(attrs.title, "Three dashes marks the spot");
 * assertEquals(body, "ferret");
 * assertEquals(frontMatter, "title: Three dashes marks the spot");
 *
 * ({ attrs, body, frontMatter } = extractYamlOrJson<{ title: string }>(
 * `---json
 * {"title": "Three dashes followed by format marks the spot"}
 * ---
 * goat`));
 * assertEquals(attrs.title, "Three dashes followed by format marks the spot");
 * assertEquals(body, "goat");
 * assertEquals(frontMatter, `{"title": "Three dashes followed by format marks the spot"}`);
 * ```
 */
export function createExtractor(
  formats: Partial<Record<Format, Parser>>,
): Extractor {
  const formatKeys = Object.keys(formats) as Format[];

  return function extract<T>(str: string): Extract<T> {
    const format = recognize(str, formatKeys);
    const parser = formats[format];

    if (format === "unknown" || !parser) {
      throw new TypeError(`Unsupported front matter format`);
    }

    return _extract(str, EXTRACT_REGEXP_MAP[format], parser);
  };
}

// denoCacheMetadata={"headers":{"vary":"Origin","cf-ray":"a223e656afc17db6-LAX","x-jsr-backend":"modules","report-to":"{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=D%2FOxCR9DQ1hLQlaaXUc3p0y9tgnmmKbEjZYXiy4BVx0a1V0Zirw%2F%2FaBKxD1HFMt4PYVi%2BhVcrlm5sNbbhnAXB6p6naxIv1M3eYAh%2BQfaOv5GT6jkcFxXRq8%3D\"}]}","nel":"{\"report_to\":\"cf-nel\",\"success_fraction\":0.0,\"max_age\":604800}","alt-svc":"h3=\":443\"; ma=86400","content-length":"5865","cache-control":"public, max-age=31536000, immutable","x-content-type-options":"nosniff","set-cookie":"__cf_bm=rf6V.rHoOnTZ9UW5GMy1oSHzOMFGkALrLBInKd83OtA-1785241874.9939559-1.0.1.1-uTsTkrLL30Lo8iHjYQzxjFqgPEiCZkT4cRJ8RxO9JN8KX6OCj_csiOZREQMGrNUX0ZGJ7vUNo2aBg5Dkps9gg2UW0YR_dHhhZoVcQkjfwT_aLbQhjsFqed5uHbUaTBB5; HttpOnly; SameSite=None; Secure; Path=/; Domain=jsr.io; Expires=Tue, 28 Jul 2026 13:01:15 GMT","access-control-allow-origin":"*","server":"cloudflare","content-security-policy":"default-src 'none'; script-src 'none'; style-src 'none'; img-src 'none'; font-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'; frame-ancestors 'none'; sandbox; form-action 'none';","date":"Tue, 28 Jul 2026 12:31:15 GMT","content-type":"text/typescript","etag":"\"adeaabca925f9b25fd5ff1f9a7979152\"","access-control-expose-headers":"*","cross-origin-resource-policy":"cross-origin","x-robots-tag":"noindex"},"url":"https://jsr.io/@std/front-matter/0.224.3/create_extractor.ts","time":1785241875}