Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(front_matter): Move exports to their own module #3634

Merged
merged 7 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions front_matter/_formats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

type Delimiter = string | [begin: string, end: string];

export enum Format {
YAML = "yaml",
TOML = "toml",
JSON = "json",
UNKNOWN = "unknown",
}

const { isArray } = Array;

function getBeginToken(delimiter: Delimiter): string {
return isArray(delimiter) ? delimiter[0] : delimiter;
}

function getEndToken(delimiter: Delimiter): string {
return isArray(delimiter) ? delimiter[1] : delimiter;
}

function createRegExp(...dv: Delimiter[]): [RegExp, RegExp] {
const beginPattern = "(" + dv.map(getBeginToken).join("|") + ")";
const pattern = "^(" +
"\\ufeff?" + // Maybe byte order mark
beginPattern +
"$([\\s\\S]+?)" +
"^(?:" + dv.map(getEndToken).join("|") + ")\\s*" +
"$" +
(globalThis?.Deno?.build?.os === "windows" ? "\\r?" : "") +
"(?:\\n)?)";

Check warning on line 31 in front_matter/_formats.ts

View check run for this annotation

Codecov / codecov/patch

front_matter/_formats.ts#L27-L31

Added lines #L27 - L31 were not covered by tests

return [
new RegExp("^" + beginPattern + "$", "im"),
new RegExp(pattern, "im"),
];
}

const [RX_RECOGNIZE_YAML, RX_YAML] = createRegExp(
["---yaml", "---"],
"= yaml =",
"---",
);
const [RX_RECOGNIZE_TOML, RX_TOML] = createRegExp(
["---toml", "---"],
"\\+\\+\\+",
"= toml =",
);
const [RX_RECOGNIZE_JSON, RX_JSON] = createRegExp(
["---json", "---"],
"= json =",
);

export const MAP_FORMAT_TO_RECOGNIZER_RX: Omit<
Record<Format, RegExp>,
Format.UNKNOWN
> = {
[Format.YAML]: RX_RECOGNIZE_YAML,
[Format.TOML]: RX_RECOGNIZE_TOML,
[Format.JSON]: RX_RECOGNIZE_JSON,
};

export const MAP_FORMAT_TO_EXTRACTOR_RX: Omit<
Record<Format, RegExp>,
Format.UNKNOWN
> = {
[Format.YAML]: RX_YAML,
[Format.TOML]: RX_TOML,
[Format.JSON]: RX_JSON,
};
2 changes: 1 addition & 1 deletion front_matter/_test_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { assert, assertEquals, assertThrows } from "../assert/mod.ts";
import { dirname, fromFileUrl, join, resolve } from "../path/mod.ts";
import { Format } from "./mod.ts";
import { Format } from "./_formats.ts";

const moduleDir = dirname(fromFileUrl(import.meta.url));
const testdataDir = resolve(moduleDir, "testdata");
Expand Down
6 changes: 4 additions & 2 deletions front_matter/any.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

import { createExtractor, Format, Parser, test as _test } from "./mod.ts";
import { createExtractor, Parser } from "./create_extractor.ts";
import { Format } from "./_formats.ts";
import { parse as parseYAML } from "../yaml/parse.ts";
import { parse as parseTOML } from "../toml/parse.ts";

export { Format, test } from "./mod.ts";
export { Format } from "./_formats.ts";
export { test } from "./test.ts";
export const extract = createExtractor({
[Format.YAML]: parseYAML as Parser,
[Format.TOML]: parseTOML as Parser,
Expand Down
134 changes: 134 additions & 0 deletions front_matter/create_extractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

import {
Format,
MAP_FORMAT_TO_EXTRACTOR_RX,
MAP_FORMAT_TO_RECOGNIZER_RX,
} from "./_formats.ts";

export type Extract<T> = {
frontMatter: string;
body: string;
attrs: T;
};

export type Extractor = <T = Record<string, unknown>>(
str: string,
) => Extract<T>;

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 YAML, TOML and JSON.
*
* @param str String to recognize.
* @param formats A list of formats to recognize. Defaults to all supported formats.
*
* ```ts
* import { recognize, Format } from "https://deno.land/std@$STD_VERSION/front_matter/mod.ts";
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/assert/assert_equals.ts";
*
* assertEquals(recognize("---\ntitle: Three dashes marks the spot\n---\n"), Format.YAML);
* assertEquals(recognize("---toml\ntitle = 'Three dashes followed by format marks the spot'\n---\n"), Format.TOML);
* assertEquals(recognize("---json\n{\"title\": \"Three dashes followed by format marks the spot\"}\n---\n"), Format.JSON);
* assertEquals(recognize("---xml\n<title>Three dashes marks the spot</title>\n---\n"), Format.UNKNOWN);
*
* assertEquals(recognize("---json\n<title>Three dashes marks the spot</title>\n---\n", [Format.YAML]), Format.UNKNOWN);
*/
function recognize(str: string, formats?: Format[]): Format {
if (!formats) {
formats = Object.keys(MAP_FORMAT_TO_RECOGNIZER_RX) as Format[];
}

Check warning on line 56 in front_matter/create_extractor.ts

View check run for this annotation

Codecov / codecov/patch

front_matter/create_extractor.ts#L55-L56

Added lines #L55 - L56 were not covered by tests

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

for (const format of formats) {
if (format === Format.UNKNOWN) {
continue;
}

Check warning on line 63 in front_matter/create_extractor.ts

View check run for this annotation

Codecov / codecov/patch

front_matter/create_extractor.ts#L62-L63

Added lines #L62 - L63 were not covered by tests

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

return Format.UNKNOWN;
}

/**
* Factory that creates a function that extracts front matter from a string with the given parsers.
* Supports YAML, TOML and JSON.
*
* @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.
*
* ```ts
* import { createExtractor, Format, Parser } from "https://deno.land/std@$STD_VERSION/front_matter/mod.ts";
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/assert/assert_equals.ts";
* import { parse as parseYAML } from "https://deno.land/std@$STD_VERSION/yaml/parse.ts";
* import { parse as parseTOML } from "https://deno.land/std@$STD_VERSION/toml/parse.ts";
* const extractYAML = createExtractor({ [Format.YAML]: parseYAML as Parser });
* const extractTOML = createExtractor({ [Format.TOML]: parseTOML as Parser });
* const extractJSON = createExtractor({ [Format.JSON]: JSON.parse as Parser });
* const extractYAMLOrJSON = createExtractor({
* [Format.YAML]: parseYAML as Parser,
* [Format.JSON]: JSON.parse as Parser,
* });
*
* let { attrs, body, frontMatter } = extractYAML<{ title: string }>("---\ntitle: Three dashes marks the spot\n---\nferret");
* assertEquals(attrs.title, "Three dashes marks the spot");
* assertEquals(body, "ferret");
* assertEquals(frontMatter, "title: Three dashes marks the spot");
*
* ({ attrs, body, frontMatter } = extractTOML<{ title: string }>("---toml\ntitle = 'Three dashes followed by format marks the spot'\n---\n"));
* assertEquals(attrs.title, "Three dashes followed by format marks the spot");
* assertEquals(body, "");
* assertEquals(frontMatter, "title = 'Three dashes followed by format marks the spot'");
*
* ({ attrs, body, frontMatter } = extractJSON<{ title: string }>("---json\n{\"title\": \"Three dashes followed by format marks the spot\"}\n---\ngoat"));
* 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\"}");
*
* ({ attrs, body, frontMatter } = extractYAMLOrJSON<{ title: string }>("---\ntitle: Three dashes marks the spot\n---\nferret"));
* 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\n{\"title\": \"Three dashes followed by format marks the spot\"}\n---\ngoat"));
* 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 === Format.UNKNOWN || !parser) {
throw new TypeError(`Unsupported front matter format`);
}

return _extract(str, MAP_FORMAT_TO_EXTRACTOR_RX[format], parser);
};
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

import { assert, assertThrows } from "../assert/mod.ts";
import { createExtractor, Format, Parser, test } from "./mod.ts";
import { assertThrows } from "../assert/mod.ts";
import { Format } from "./_formats.ts";
import { parse as parseYAML } from "../yaml/parse.ts";
import { parse as parseTOML } from "../toml/parse.ts";
import {
Expand All @@ -11,9 +11,8 @@ import {
runExtractTypeErrorTests,
runExtractYAMLTests1,
runExtractYAMLTests2,
runTestInvalidInputTests,
runTestValidInputTests,
} from "./_test_utils.ts";
import { createExtractor, Parser } from "./create_extractor.ts";

const extractYAML = createExtractor({ [Format.YAML]: parseYAML as Parser });
const extractTOML = createExtractor({ [Format.TOML]: parseTOML as Parser });
Expand All @@ -28,26 +27,8 @@ const extractAny = createExtractor({
[Format.TOML]: parseTOML as Parser,
});

// GENERAL TESTS //

Deno.test("[ANY] try to test for unknown format", () => {
assertThrows(
() => test("foo", [Format.UNKNOWN]),
TypeError,
"Unable to test for unknown front matter format",
);
});

// YAML //

Deno.test("[YAML] test valid input true", () => {
runTestValidInputTests(Format.YAML, test);
});

Deno.test("[YAML] test invalid input false", () => {
runTestInvalidInputTests(Format.YAML, test);
});

Deno.test("[YAML] extract type error on invalid input", () => {
runExtractTypeErrorTests(Format.YAML, extractYAML);
});
Expand All @@ -67,7 +48,6 @@ Deno.test({
resolveTestDataPath("./horizontal_rules.md"),
);

assert(!test(str));
assertThrows(
() => {
extractAny(str);
Expand All @@ -80,14 +60,6 @@ Deno.test({

// JSON //

Deno.test("[JSON] test valid input true", () => {
runTestValidInputTests(Format.JSON, test);
});

Deno.test("[JSON] test invalid input false", () => {
runTestInvalidInputTests(Format.JSON, test);
});

Deno.test("[JSON] extract type error on invalid input", () => {
runExtractTypeErrorTests(Format.JSON, extractJSON);
});
Expand All @@ -98,14 +70,6 @@ Deno.test("[JSON] parse json delineate by ---json", async () => {

// TOML //

Deno.test("[TOML] test valid input true", () => {
runTestValidInputTests(Format.TOML, test);
});

Deno.test("[TOML] test invalid input false", () => {
runTestInvalidInputTests(Format.TOML, test);
});

Deno.test("[TOML] extract type error on invalid input", () => {
runExtractTypeErrorTests(Format.TOML, extractTOML);
});
Expand Down
7 changes: 5 additions & 2 deletions front_matter/json.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

import { createExtractor, Format, Parser, test as _test } from "./mod.ts";
export { Format } from "./mod.ts";
import { createExtractor, Parser } from "./create_extractor.ts";
import { Format } from "./_formats.ts";
import { test as _test } from "./test.ts";

export { Format } from "./_formats.ts";

export function test(str: string): boolean {
return _test(str, [Format.JSON]);
Expand Down
Loading