Repository: graphql-markdown/graphql-markdown
Pull request #1176 (2023-12-26 15:24:21)

:sparkles:  add list and object support for frontMatter option
# Description

Add list and object support for `frontMatter` option needed for #1168.

# Checklist

- [x] My changes follow the [contributing guidelines](https://github.com/graphql-markdown/graphql-markdown/blob/main/CONTRIBUTING.md) of this project.
- [x] I have performed a self-review of my code.
- [ ] I have made corresponding changes to the documentation.
- [x] I have added tests that prove my fix is effective or that my changes work.
- [x] New and existing unit tests pass locally with my changes.


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=graphql-markdown_graphql-markdown&pullRequest=1176) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=1176&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=graphql-markdown_graphql-markdown&pullRequest=1176&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=graphql-markdown_graphql-markdown&pullRequest=1176&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=graphql-markdown_graphql-markdown&pullRequest=1176)


codesee-maps[bot]: <details open>
<summary><b>👇 <a href="https://app.codesee.io/r/reviews?pr=1176&src=https%3A%2F%2Fgithub.com%2Fgraphql-markdown%2Fgraphql-markdown">Click on the image</a> for a new way to code review</b>
</summary>

####

[![Review these changes using an interactive CodeSee Map](https://s3.us-east-2.amazonaws.com/maps.codesee.io/images/github/graphql-markdown/graphql-markdown/1176/7e2564e0/4cd55bb66676223a1810e5db8f35d8966d2b75bb.svg)](https://app.codesee.io/r/reviews?pr=1176&src=https%3A%2F%2Fgithub.com%2Fgraphql-markdown%2Fgraphql-markdown)

#### Legend
<img src="https://app.codesee.io/Legend_v3.png" alt="CodeSee Map legend" width="220" height="156">
</details>
coveralls: 
[![Coverage Status](https://coveralls.io/builds/64762760/badge)](https://coveralls.io/builds/64762760)

coverage: 93.963% (-0.2%) from 94.198%
when pulling **4cd55bb66676223a1810e5db8f35d8966d2b75bb on feat/frontmatter-list**
into **331336b95d6068b6c9a16eb3c3bbc0f83bb4739c on main**.

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=graphql-markdown_graphql-markdown&pullRequest=1176) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=1176&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=graphql-markdown_graphql-markdown&pullRequest=1176&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=graphql-markdown_graphql-markdown&pullRequest=1176&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=graphql-markdown_graphql-markdown&pullRequest=1176)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=graphql-markdown_graphql-markdown&pullRequest=1176) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=graphql-markdown_graphql-markdown&pullRequest=1176&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=graphql-markdown_graphql-markdown&pullRequest=1176&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=graphql-markdown_graphql-markdown&pullRequest=1176&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=graphql-markdown_graphql-markdown&pullRequest=1176)



Patch:
diff --git a/packages/printer-legacy/src/const/strings.ts b/packages/printer-legacy/src/const/strings.ts
index d3836f21..11a6b4e9 100644
--- a/packages/printer-legacy/src/const/strings.ts
+++ b/packages/printer-legacy/src/const/strings.ts
@@ -16,7 +16,7 @@ export const ROOT_TYPE_LOCALE: RootTypeLocale = {
 
 export const NO_DESCRIPTION_TEXT = "No description" as const;
 
-export const FRONT_MATTER = "---" as const;
+export const FRONT_MATTER_DELIMITER = "---" as const;
 export const MARKDOWN_CODE_INDENTATION = "  " as const;
 export const MARKDOWN_EOL = "\n" as const;
 export const MARKDOWN_EOP = "\n\n" as const;
diff --git a/packages/printer-legacy/src/frontmatter.ts b/packages/printer-legacy/src/frontmatter.ts
new file mode 100644
index 00000000..98fa8b83
--- /dev/null
+++ b/packages/printer-legacy/src/frontmatter.ts
@@ -0,0 +1,97 @@
+import type { FrontMatterOptions, Maybe } from "@graphql-markdown/types";
+import {
+  FRONT_MATTER_DELIMITER,
+  MARKDOWN_CODE_INDENTATION,
+  MARKDOWN_EOL,
+} from "./const/strings";
+
+const tabs = (indentation: number = 0): string => {
+  return MARKDOWN_CODE_INDENTATION.repeat(indentation);
+};
+
+const formatFrontMatterObject = (
+  props: unknown,
+  indentation: number = 0,
+  prefix?: string,
+): string[] => {
+  const frontMatter: string[] = [];
+
+  if (!props || typeof props !== "object") {
+    return frontMatter;
+  }
+
+  for (const [key, value] of Object.entries(props)) {
+    frontMatter.push(
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      ...formatFrontMatterProp({ [key]: value }, indentation + 1, prefix),
+    );
+    prefix = " ".repeat(prefix?.length ?? 0);
+  }
+
+  return frontMatter;
+};
+
+const formatFrontMatterList = (
+  prop: unknown,
+  indentation: number = 0,
+  prefix: string = "- ",
+): string[] => {
+  const frontMatter: string[] = [];
+
+  if (!Array.isArray(prop)) {
+    return frontMatter;
+  }
+
+  prop.forEach((entry: unknown) => {
+    if (typeof entry === "object") {
+      frontMatter.push(
+        ...formatFrontMatterObject(entry, -1, prefix).map((item) => {
+          return `${tabs(indentation)}${item}`;
+        }),
+      );
+    } else {
+      frontMatter.push(`${tabs(indentation)}- ${entry}`);
+    }
+  });
+
+  return frontMatter;
+};
+
+export const formatFrontMatterProp = (
+  prop: unknown,
+  indentation: number = 0,
+  prefix?: string,
+): string[] => {
+  if (typeof prop === "undefined" || prop === null) {
+    return [];
+  }
+
+  const tabs = MARKDOWN_CODE_INDENTATION.repeat(indentation);
+  const [key, value] = Object.entries(prop)[0];
+  const index = `${tabs}${prefix ?? ""}${key}:`;
+
+  switch (true) {
+    case typeof value !== "string" && Array.isArray(value):
+      return [index, ...formatFrontMatterList(value, indentation + 1, prefix)];
+    case typeof value === "object" && value !== null:
+      return [index, ...formatFrontMatterObject(value, indentation, prefix)];
+    default:
+      return [`${index} ${value}`];
+  }
+};
+
+export const printFrontMatter = (
+  id: string,
+  title: string,
+  props?: Maybe<FrontMatterOptions>,
+): string => {
+  const frontMatter = formatFrontMatterObject({ ...props, id, title }, -1);
+
+  const header = [
+    FRONT_MATTER_DELIMITER,
+    ...frontMatter,
+    FRONT_MATTER_DELIMITER,
+  ];
+
+  return header.join(MARKDOWN_EOL);
+};
diff --git a/packages/printer-legacy/src/printer.ts b/packages/printer-legacy/src/printer.ts
index e64a75fd..618a3f89 100644
--- a/packages/printer-legacy/src/printer.ts
+++ b/packages/printer-legacy/src/printer.ts
@@ -35,6 +35,7 @@ import { pathUrl } from "@graphql-markdown/utils";
 import { printRelations } from "./relation";
 import { hasPrintableDirective, printDescription } from "./common";
 import { printCustomDirectives, printCustomTags } from "./directive";
+import { printFrontMatter } from "./frontmatter";
 import {
   printCodeDirective,
   printCodeEnum,
@@ -55,7 +56,6 @@ import {
 } from "./graphql";
 
 import {
-  FRONT_MATTER,
   MARKDOWN_EOC,
   MARKDOWN_EOL,
   MARKDOWN_EOP,
@@ -134,18 +134,9 @@ export class Printer implements IPrinter {
     title: string,
     options: PrintTypeOptions,
   ): string => {
-    const frontMatter = options.frontMatter ?? DEFAULT_OPTIONS.frontMatter!;
+    const fmOptions = options.frontMatter ?? DEFAULT_OPTIONS.frontMatter;
 
-    const header: string[] = [FRONT_MATTER, `id: ${id}`, `title: ${title}`];
-
-    if (typeof frontMatter === "object") {
-      for (const [key, value] of Object.entries(frontMatter)) {
-        header.push(`${key}: ${value}`);
-      }
-    }
-    header.push(FRONT_MATTER);
-
-    return header.join(MARKDOWN_EOL);
+    return printFrontMatter(id, title, fmOptions);
   };
 
   static printCode = (type: unknown, options: PrintTypeOptions): string => {


Test patch:
diff --git a/packages/printer-legacy/tests/unit/frontmatter.test.ts b/packages/printer-legacy/tests/unit/frontmatter.test.ts
new file mode 100644
index 00000000..99fb0e95
--- /dev/null
+++ b/packages/printer-legacy/tests/unit/frontmatter.test.ts
@@ -0,0 +1,101 @@
+import { formatFrontMatterProp } from "../../src/frontmatter";
+
+describe("frontMatter", () => {
+  describe("formatFrontMatterProp", () => {
+    test("returns empty structure if undefined", () => {
+      expect.hasAssertions();
+
+      expect(formatFrontMatterProp(undefined)).toStrictEqual([]);
+    });
+
+    test("returns flat structure for simple object", () => {
+      expect.hasAssertions();
+
+      expect(formatFrontMatterProp({ item: "test" })).toStrictEqual([
+        "item: test",
+      ]);
+    });
+
+    test("returns null for null object", () => {
+      expect.hasAssertions();
+
+      expect(formatFrontMatterProp({ item: null })).toStrictEqual([
+        "item: null",
+      ]);
+    });
+
+    test("returns indented structure for nested object", () => {
+      expect.hasAssertions();
+
+      expect(formatFrontMatterProp({ item: { foo: "test" } })).toStrictEqual([
+        "item:",
+        "  foo: test",
+      ]);
+    });
+
+    test("returns indented structure for deep nested object", () => {
+      expect.hasAssertions();
+
+      expect(
+        formatFrontMatterProp({ level1: { level2: { level3: "value" } } }),
+      ).toStrictEqual(["level1:", "  level2:", "    level3: value"]);
+    });
+
+    test("returns indented list for array", () => {
+      expect.hasAssertions();
+
+      expect(formatFrontMatterProp({ item: ["a", "b", "c"] })).toStrictEqual([
+        "item:",
+        "  - a",
+        "  - b",
+        "  - c",
+      ]);
+    });
+
+    test("returns indented structure for object with array", () => {
+      expect.hasAssertions();
+
+      expect(formatFrontMatterProp({ item: { bar: ["value"] } })).toStrictEqual(
+        ["item:", "  bar:", "    - value"],
+      );
+    });
+
+    test("returns indented structure for array with object", () => {
+      expect.hasAssertions();
+
+      expect(formatFrontMatterProp({ item: [{ foo: "test" }] })).toStrictEqual([
+        "item:",
+        "  - foo: test",
+      ]);
+    });
+
+    test("returns indented structure for complex object", () => {
+      expect.hasAssertions();
+
+      expect(
+        formatFrontMatterProp({
+          item: [
+            {
+              foo: "test",
+              bar: ["a", "b", "c"],
+              baz: { foo: "baz", zap: ["a", "b", "c"] },
+            },
+          ],
+        }),
+      ).toStrictEqual([
+        "item:",
+        "  - foo: test",
+        "    bar:",
+        "    - a",
+        "    - b",
+        "    - c",
+        "    baz:",
+        "      foo: baz",
+        "      zap:",
+        "      - a",
+        "      - b",
+        "      - c",
+      ]);
+    });
+  });
+});
diff --git a/packages/printer-legacy/tests/unit/printer.test.ts b/packages/printer-legacy/tests/unit/printer.test.ts
index e38f57a4..ec3ca3ba 100644
--- a/packages/printer-legacy/tests/unit/printer.test.ts
+++ b/packages/printer-legacy/tests/unit/printer.test.ts
@@ -304,10 +304,10 @@ describe("Printer", () => {
 
       expect(header).toMatchInlineSnapshot(`
             "---
-            id: an-object-type-name
-            title: An Object Type Name
             draft: true
             hide_table_of_contents: null
+            id: an-object-type-name
+            title: An Object Type Name
             ---"
           `);
     });