zudoku 0.23.4 → 0.23.6

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.
Files changed (59) hide show
  1. package/dist/config/validators/common.d.ts +37 -37
  2. package/dist/config/validators/common.js +4 -1
  3. package/dist/config/validators/common.js.map +1 -1
  4. package/dist/config/validators/validate.d.ts +14 -14
  5. package/dist/lib/components/navigation/SidebarBadge.d.ts +0 -9
  6. package/dist/lib/components/navigation/SidebarBadge.js +0 -9
  7. package/dist/lib/components/navigation/SidebarBadge.js.map +1 -1
  8. package/dist/lib/plugins/openapi/OperationList.js +10 -34
  9. package/dist/lib/plugins/openapi/OperationList.js.map +1 -1
  10. package/dist/lib/plugins/openapi/OperationListItem.d.ts +2 -1
  11. package/dist/lib/plugins/openapi/OperationListItem.js +14 -4
  12. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  13. package/dist/lib/plugins/openapi/Route.d.ts +4 -2
  14. package/dist/lib/plugins/openapi/Route.js +25 -2
  15. package/dist/lib/plugins/openapi/Route.js.map +1 -1
  16. package/dist/lib/plugins/openapi/Sidecar.js +2 -12
  17. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  18. package/dist/lib/plugins/openapi/context.d.ts +3 -3
  19. package/dist/lib/plugins/openapi/index.js +12 -12
  20. package/dist/lib/plugins/openapi/index.js.map +1 -1
  21. package/dist/lib/plugins/openapi/interfaces.d.ts +19 -0
  22. package/dist/lib/plugins/openapi/util/methodToColor.d.ts +20 -0
  23. package/dist/lib/plugins/openapi/util/methodToColor.js +24 -0
  24. package/dist/lib/plugins/openapi/util/methodToColor.js.map +1 -0
  25. package/dist/lib/plugins/openapi/util/sanitizeMarkdownForMetatag.d.ts +1 -0
  26. package/dist/lib/plugins/openapi/util/sanitizeMarkdownForMetatag.js +27 -0
  27. package/dist/lib/plugins/openapi/util/sanitizeMarkdownForMetatag.js.map +1 -0
  28. package/dist/vite/config.js +2 -1
  29. package/dist/vite/config.js.map +1 -1
  30. package/dist/vite/plugin-api.js +39 -15
  31. package/dist/vite/plugin-api.js.map +1 -1
  32. package/lib/{OperationList-C7ac3kR5.js → OperationList-n4U_BHmO.js} +1886 -1838
  33. package/lib/OperationList-n4U_BHmO.js.map +1 -0
  34. package/lib/Route-C8nwd9A2.js +37 -0
  35. package/lib/Route-C8nwd9A2.js.map +1 -0
  36. package/lib/StaggeredRender-DgsamH_G.js +17 -0
  37. package/lib/StaggeredRender-DgsamH_G.js.map +1 -0
  38. package/lib/context-h_UkBLvr.js.map +1 -1
  39. package/lib/{index-C8ubT49C.js → index-DStSNvP-.js} +442 -437
  40. package/lib/{index-C8ubT49C.js.map → index-DStSNvP-.js.map} +1 -1
  41. package/lib/zudoku.components.js +385 -363
  42. package/lib/zudoku.components.js.map +1 -1
  43. package/lib/zudoku.plugin-openapi.js +1 -1
  44. package/package.json +1 -1
  45. package/src/lib/components/navigation/SidebarBadge.tsx +0 -10
  46. package/src/lib/plugins/openapi/OperationList.tsx +45 -50
  47. package/src/lib/plugins/openapi/OperationListItem.tsx +31 -2
  48. package/src/lib/plugins/openapi/Route.tsx +45 -9
  49. package/src/lib/plugins/openapi/Sidecar.tsx +2 -16
  50. package/src/lib/plugins/openapi/context.tsx +2 -2
  51. package/src/lib/plugins/openapi/index.tsx +35 -28
  52. package/src/lib/plugins/openapi/interfaces.ts +22 -1
  53. package/src/lib/plugins/openapi/util/methodToColor.ts +27 -0
  54. package/src/lib/plugins/openapi/util/sanitizeMarkdownForMetatag.tsx +32 -0
  55. package/lib/OperationList-C7ac3kR5.js.map +0 -1
  56. package/lib/Route-C9cYcP-j.js +0 -11
  57. package/lib/Route-C9cYcP-j.js.map +0 -1
  58. package/lib/SidebarBadge-Bm793GDY.js +0 -51
  59. package/lib/SidebarBadge-Bm793GDY.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import "./jsx-runtime-Dx-03ztt.js";
2
2
  import "./chunk-D52XG6IA-Dl7HLe6j.js";
3
- import { o as a } from "./index-C8ubT49C.js";
3
+ import { o as a } from "./index-DStSNvP-.js";
4
4
  import "./utils-B4O1uet5.js";
5
5
  import "lucide-react";
6
6
  import "./hook-DgGeo5iL.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zudoku",
3
- "version": "0.23.4",
3
+ "version": "0.23.6",
4
4
  "type": "module",
5
5
  "homepage": "https://zudoku.dev",
6
6
  "repository": {
@@ -1,15 +1,5 @@
1
1
  import { cn } from "../../util/cn.js";
2
2
 
3
- export const TextColorMap = {
4
- green: "text-green-600",
5
- blue: "text-sky-600",
6
- yellow: "text-yellow-600",
7
- red: "text-red-600",
8
- purple: "text-purple-600",
9
- indigo: "text-indigo-600",
10
- gray: "text-gray-600",
11
- };
12
-
13
3
  export const ColorMap = {
14
4
  green: "bg-green-400 dark:bg-green-800",
15
5
  blue: "bg-sky-400 dark:bg-sky-800",
@@ -1,6 +1,15 @@
1
1
  import { ResultOf } from "@graphql-typed-document-node/core";
2
2
  import { useSuspenseQuery } from "@tanstack/react-query";
3
3
  import { Helmet } from "@zudoku/react-helmet-async";
4
+ import { useNavigate } from "react-router";
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from "zudoku/ui/Select.js";
12
+ import { useSelectedServerStore } from "../../authentication/state.js";
4
13
  import { CategoryHeading } from "../../components/CategoryHeading.js";
5
14
  import { Heading } from "../../components/Heading.js";
6
15
  import { Markdown, ProseClasses } from "../../components/Markdown.js";
@@ -11,6 +20,7 @@ import StaggeredRender from "./StaggeredRender.js";
11
20
  import { useCreateQuery } from "./client/useCreateQuery.js";
12
21
  import { useOasConfig } from "./context.js";
13
22
  import { graphql } from "./graphql/index.js";
23
+ import { sanitizeMarkdownForMetatag } from "./util/sanitizeMarkdownForMetatag.js";
14
24
 
15
25
  export const OperationsFragment = graphql(/* GraphQL */ `
16
26
  fragment OperationsFragment on OperationItem {
@@ -98,63 +108,25 @@ const AllOperationsQuery = graphql(/* GraphQL */ `
98
108
  }
99
109
  `);
100
110
 
101
- /**
102
- * @description Clean up a commonmark formatted description for use in the meta
103
- * description.
104
- */
105
- function cleanDescription(
106
- description: string,
107
- maxLength: number = 160,
108
- ): string {
109
- if (!description) {
110
- return "";
111
- }
112
-
113
- // Replace Markdown links [text](url) with just "text"
114
- description = description.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
115
-
116
- // Remove Markdown image syntax: ![alt](url)
117
- description = description.replace(/!\[.*?\]\(.*?\)/g, "");
118
-
119
- // Remove other Markdown syntax (e.g., **bold**, _italic_, `code`)
120
- description = description.replace(/[_*`~]/g, "");
121
-
122
- // Remove headings (# Heading), blockquotes (> Quote), and horizontal rules (--- or ***)
123
- description = description.replace(/^(?:>|\s*#+|-{3,}|\*{3,})/gm, "");
124
-
125
- // Remove any remaining formatting characters
126
- description = description.replace(/[|>{}[\]]/g, "");
127
-
128
- // Collapse multiple spaces and trim the text
129
- description = description.replace(/\s+/g, " ").trim();
130
-
131
- // Limit to the specified maximum length
132
- description = description.substring(0, maxLength);
133
-
134
- // Escape for HTML safety
135
- return description
136
- .replace(/&/g, "&")
137
- .replace(/</g, "&lt;")
138
- .replace(/>/g, "&gt;")
139
- .replace(/"/g, "&quot;")
140
- .replace(/'/g, "&#039;");
141
- }
142
-
143
111
  export const OperationList = () => {
144
- const { input, type } = useOasConfig();
112
+ const { input, type, versions, version } = useOasConfig();
145
113
  const query = useCreateQuery(AllOperationsQuery, { input, type });
114
+ const { selectedServer } = useSelectedServerStore();
146
115
  const result = useSuspenseQuery(query);
147
116
  const title = result.data.schema.title;
148
117
  const summary = result.data.schema.summary;
149
118
  const description = result.data.schema.description;
119
+ const navigate = useNavigate();
120
+
150
121
  // The summary property is preferable here as it is a short description of
151
122
  // the API, whereas the description property is typically longer and supports
152
123
  // commonmark formatting, making it ill-suited for use in the meta description
153
124
  const metaDescription = summary
154
125
  ? summary
155
126
  : description
156
- ? cleanDescription(description)
127
+ ? sanitizeMarkdownForMetatag(description)
157
128
  : undefined;
129
+
158
130
  return (
159
131
  <div className="pt-[--padding-content-top]">
160
132
  <Helmet>
@@ -166,17 +138,39 @@ export const OperationList = () => {
166
138
  <div
167
139
  className={cn(ProseClasses, "mb-16 max-w-full prose-img:max-w-prose")}
168
140
  >
169
- <CategoryHeading>Overview</CategoryHeading>
170
- <Heading level={1} id="description" registerSidebarAnchor>
171
- {title}
172
- </Heading>
141
+ <div className="flex">
142
+ <div className="flex-1">
143
+ <CategoryHeading>Overview</CategoryHeading>
144
+ <Heading level={1} id="description" registerSidebarAnchor>
145
+ {title}
146
+ </Heading>
147
+ </div>
148
+ <div>
149
+ {Object.entries(versions).length > 1 && (
150
+ <Select
151
+ onValueChange={(version) => navigate(versions[version]!)}
152
+ defaultValue={version}
153
+ >
154
+ <SelectTrigger className="w-[180px]">
155
+ <SelectValue placeholder="Select version" />
156
+ </SelectTrigger>
157
+ <SelectContent>
158
+ {Object.entries(versions).map(([version]) => (
159
+ <SelectItem key={version} value={version}>
160
+ {version}
161
+ </SelectItem>
162
+ ))}
163
+ </SelectContent>
164
+ </Select>
165
+ )}
166
+ </div>
167
+ </div>
173
168
  <Markdown content={result.data.schema.description ?? ""} />
174
169
  </div>
175
170
  <hr />
176
- <div className="my-4 flex justify-end">
171
+ <div className="my-4 flex items-center justify-end gap-4">
177
172
  <Endpoint />
178
173
  </div>
179
-
180
174
  {result.data.schema.tags
181
175
  .filter((tag) => tag.operations.length > 0)
182
176
  .map((tag) => (
@@ -193,6 +187,7 @@ export const OperationList = () => {
193
187
  <StaggeredRender>
194
188
  {tag.operations.map((fragment) => (
195
189
  <OperationListItem
190
+ serverUrl={selectedServer ?? result.data.schema.url}
196
191
  key={fragment.slug}
197
192
  operationFragment={fragment}
198
193
  />
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useRef, useState } from "react";
2
2
  import { Heading } from "../../components/Heading.js";
3
3
  import { Markdown, ProseClasses } from "../../components/Markdown.js";
4
4
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/Tabs.js";
@@ -9,20 +9,24 @@ import { ParameterList } from "./ParameterList.js";
9
9
  import { Sidecar } from "./Sidecar.js";
10
10
  import { FragmentType, useFragment } from "./graphql/index.js";
11
11
  import { SchemaView } from "./schema/SchemaView.js";
12
+ import { methodForColor } from "./util/methodToColor.js";
12
13
 
13
14
  export const PARAM_GROUPS = ["path", "query", "header", "cookie"] as const;
14
15
  export type ParameterGroup = (typeof PARAM_GROUPS)[number];
15
16
 
16
17
  export const OperationListItem = ({
17
18
  operationFragment,
19
+ serverUrl,
18
20
  }: {
19
21
  operationFragment: FragmentType<typeof OperationsFragment>;
22
+ serverUrl?: string;
20
23
  }) => {
21
24
  const operation = useFragment(OperationsFragment, operationFragment);
22
25
  const groupedParameters = groupBy(
23
26
  operation.parameters ?? [],
24
27
  (param) => param.in,
25
28
  );
29
+ const parentRef = useRef<HTMLDivElement>(null);
26
30
 
27
31
  const first = operation.responses.at(0);
28
32
  const [selectedResponse, setSelectedResponse] = useState(first?.statusCode);
@@ -30,12 +34,37 @@ export const OperationListItem = ({
30
34
  return (
31
35
  <div
32
36
  key={operation.operationId}
33
- className="grid grid-cols-1 lg:grid-cols-[4fr_3fr] gap-8 items-start border-b-2 mb-16 pb-16"
37
+ className="grid grid-cols-1 lg:grid-cols-[minmax(0,4fr)_minmax(0,3fr)] gap-8 items-start border-b-2 mb-16 pb-16"
34
38
  >
35
39
  <div className="flex flex-col gap-4">
36
40
  <Heading level={2} id={operation.slug} registerSidebarAnchor>
37
41
  {operation.summary}
38
42
  </Heading>
43
+ <div className="text-sm flex gap-2 font-mono">
44
+ <span className={methodForColor(operation.method)}>
45
+ {operation.method.toUpperCase()}
46
+ </span>
47
+ <div
48
+ ref={parentRef}
49
+ className="max-w-full truncate flex cursor-pointer"
50
+ onClick={() => {
51
+ if (parentRef.current) {
52
+ const range = document.createRange();
53
+ range.selectNodeContents(parentRef.current);
54
+ const selection = window.getSelection();
55
+ selection?.removeAllRanges();
56
+ selection?.addRange(range);
57
+ }
58
+ }}
59
+ >
60
+ <div className="text-neutral-400 dark:text-neutral-500 truncate">
61
+ {serverUrl}
62
+ </div>
63
+ <div className="text-neutral-900 dark:text-neutral-200">
64
+ {operation.path}
65
+ </div>
66
+ </div>
67
+ </div>
39
68
  {operation.description && (
40
69
  <Markdown
41
70
  className={`${ProseClasses} max-w-full prose-img:max-w-prose`}
@@ -1,19 +1,55 @@
1
- import { Outlet } from "react-router";
1
+ import { Outlet, useParams } from "react-router";
2
+ import { joinPath } from "../../util/joinPath.js";
2
3
  import type { GraphQLClient } from "./client/GraphQLClient.js";
3
4
  import { GraphQLProvider } from "./client/GraphQLContext.js";
4
5
  import { OasConfigProvider } from "./context.js";
5
- import { OasPluginConfig } from "./interfaces.js";
6
+ import { type OasPluginConfig } from "./interfaces.js";
6
7
 
7
8
  export const OpenApiRoute = ({
9
+ basePath,
10
+ versions,
8
11
  config,
9
12
  client,
10
13
  }: {
14
+ basePath: string;
15
+ versions: string[];
11
16
  config: OasPluginConfig;
12
17
  client: GraphQLClient;
13
- }) => (
14
- <OasConfigProvider value={{ config }}>
15
- <GraphQLProvider client={client}>
16
- <Outlet />
17
- </GraphQLProvider>
18
- </OasConfigProvider>
19
- );
18
+ }) => {
19
+ const { version } = useParams<"version">();
20
+
21
+ const input =
22
+ config.type === "file"
23
+ ? {
24
+ type: config.type,
25
+ input: version
26
+ ? config.input[version]!
27
+ : Object.values(config.input).at(0)!,
28
+ }
29
+ : { type: config.type, input: config.input };
30
+
31
+ const currentVersion = version ?? versions.at(0);
32
+
33
+ if (!currentVersion) {
34
+ throw new Error("No version found");
35
+ }
36
+
37
+ return (
38
+ <OasConfigProvider
39
+ value={{
40
+ config: {
41
+ ...config,
42
+ version: currentVersion,
43
+ versions: Object.fromEntries(
44
+ versions.map((version) => [version, joinPath(basePath, version)]),
45
+ ),
46
+ ...input,
47
+ },
48
+ }}
49
+ >
50
+ <GraphQLProvider client={client}>
51
+ <Outlet />
52
+ </GraphQLProvider>
53
+ </OasConfigProvider>
54
+ );
55
+ };
@@ -3,7 +3,6 @@ import { HTTPSnippet } from "@zudoku/httpsnippet";
3
3
  import { Fragment, useMemo, useTransition } from "react";
4
4
  import { useSearchParams } from "react-router";
5
5
  import { useSelectedServerStore } from "../../authentication/state.js";
6
- import { TextColorMap } from "../../components/navigation/SidebarBadge.js";
7
6
  import { SyntaxHighlight } from "../../components/SyntaxHighlight.js";
8
7
  import type { SchemaObject } from "../../oas/parser/index.js";
9
8
  import { cn } from "../../util/cn.js";
@@ -20,6 +19,7 @@ import { ResponsesSidecarBox } from "./ResponsesSidecarBox.js";
20
19
  import * as SidecarBox from "./SidecarBox.js";
21
20
  import { SimpleSelect } from "./SimpleSelect.js";
22
21
  import { generateSchemaExample } from "./util/generateSchemaExample.js";
22
+ import { methodForColor } from "./util/methodToColor.js";
23
23
 
24
24
  const getConverted = (snippet: HTTPSnippet, option: string) => {
25
25
  let converted;
@@ -76,17 +76,6 @@ export const GetServerQuery = graphql(/* GraphQL */ `
76
76
  }
77
77
  `);
78
78
 
79
- const methodToColor = {
80
- get: TextColorMap.green,
81
- post: TextColorMap.blue,
82
- put: TextColorMap.yellow,
83
- delete: TextColorMap.red,
84
- patch: TextColorMap.purple,
85
- options: TextColorMap.indigo,
86
- head: TextColorMap.gray,
87
- trace: TextColorMap.gray,
88
- };
89
-
90
79
  const EXAMPLE_LANGUAGES = [
91
80
  { value: "shell", label: "cURL" },
92
81
  { value: "js", label: "JavaScript" },
@@ -114,10 +103,7 @@ export const Sidecar = ({
114
103
  const query = useCreateQuery(GetServerQuery, { input, type });
115
104
  const result = useSuspenseQuery(query);
116
105
 
117
- const methodTextColor =
118
- methodToColor[
119
- operation.method.toLocaleLowerCase() as keyof typeof methodToColor
120
- ] ?? TextColorMap.gray;
106
+ const methodTextColor = methodForColor(operation.method);
121
107
 
122
108
  const [searchParams, setSearchParams] = useSearchParams();
123
109
  const [, startTransition] = useTransition();
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext } from "react";
2
- import { OasPluginConfig } from "./interfaces.js";
2
+ import { OasPluginContext } from "./interfaces.js";
3
3
 
4
- const OasContext = createContext<{ config: OasPluginConfig } | undefined>(
4
+ const OasContext = createContext<{ config: OasPluginContext } | undefined>(
5
5
  undefined,
6
6
  );
7
7
 
@@ -1,4 +1,4 @@
1
- import { matchPath, type RouteObject } from "react-router";
1
+ import { matchPath } from "react-router";
2
2
  import { type ZudokuPlugin } from "../../core/plugins.js";
3
3
  import { graphql } from "./graphql/index.js";
4
4
 
@@ -52,6 +52,8 @@ export type OpenApiPluginOptions = OasPluginConfig & InternalOasPluginConfig;
52
52
  export const openApiPlugin = (config: OpenApiPluginOptions): ZudokuPlugin => {
53
53
  const basePath = joinPath(config.navigationId ?? "/reference");
54
54
 
55
+ const versions = config.type === "file" ? Object.keys(config.input) : [];
56
+
55
57
  const client = new GraphQLClient(config);
56
58
 
57
59
  return {
@@ -125,9 +127,14 @@ export const openApiPlugin = (config: OpenApiPluginOptions): ZudokuPlugin => {
125
127
  }
126
128
 
127
129
  try {
130
+ const version =
131
+ versions.find((v) => path === joinPath(basePath, v)) ??
132
+ Object.keys(config.input).at(0);
133
+
128
134
  const data = await client.fetch(GetCategoriesQuery, {
129
135
  type: config.type,
130
- input: config.input,
136
+ input: config.type === "file" ? config.input[version!] : config.input,
137
+ version,
131
138
  });
132
139
 
133
140
  const categories = data.schema.tags
@@ -159,32 +166,32 @@ export const openApiPlugin = (config: OpenApiPluginOptions): ZudokuPlugin => {
159
166
  return [];
160
167
  }
161
168
  },
162
- getRoutes: () =>
163
- [
164
- {
165
- async lazy() {
166
- const { OpenApiRoute } = await import("./Route.js");
167
- return {
168
- element: <OpenApiRoute client={client} config={config} />,
169
- };
170
- },
171
- children: [
172
- {
173
- path: basePath,
174
- children: [
175
- {
176
- index: true,
177
- async lazy() {
178
- const { OperationList } = await import(
179
- "./OperationList.js"
180
- );
181
- return { element: <OperationList /> };
182
- },
183
- },
184
- ],
185
- },
186
- ],
169
+ getRoutes: () => [
170
+ {
171
+ path: basePath + "/:version?",
172
+ async lazy() {
173
+ const { OpenApiRoute } = await import("./Route.js");
174
+ return {
175
+ element: (
176
+ <OpenApiRoute
177
+ basePath={basePath}
178
+ versions={versions}
179
+ client={client}
180
+ config={config}
181
+ />
182
+ ),
183
+ };
187
184
  },
188
- ] satisfies RouteObject[],
185
+ children: [
186
+ {
187
+ index: true,
188
+ async lazy() {
189
+ const { OperationList } = await import("./OperationList.js");
190
+ return { element: <OperationList /> };
191
+ },
192
+ },
193
+ ],
194
+ },
195
+ ],
189
196
  };
190
197
  };
@@ -1,6 +1,19 @@
1
1
  type OasSource =
2
2
  | { type: "url"; input: string }
3
- | { type: "file"; input: () => Promise<unknown> }
3
+ | {
4
+ type: "file";
5
+ input: {
6
+ [version: string]: () => Promise<unknown>;
7
+ };
8
+ }
9
+ | { type: "raw"; input: string };
10
+
11
+ export type ContextOasSource =
12
+ | { type: "url"; input: string }
13
+ | {
14
+ type: "file";
15
+ input: () => Promise<unknown>;
16
+ }
4
17
  | { type: "raw"; input: string };
5
18
 
6
19
  export type OasPluginConfig = {
@@ -8,3 +21,11 @@ export type OasPluginConfig = {
8
21
  navigationId?: string;
9
22
  skipPreload?: boolean;
10
23
  } & OasSource;
24
+
25
+ export type OasPluginContext = {
26
+ server?: string;
27
+ navigationId?: string;
28
+ skipPreload?: boolean;
29
+ version: string;
30
+ versions: Record<string, string>;
31
+ } & ContextOasSource;
@@ -0,0 +1,27 @@
1
+ export const TextColorMap = {
2
+ green: "text-green-600",
3
+ blue: "text-sky-600",
4
+ yellow: "text-yellow-600",
5
+ red: "text-red-600",
6
+ purple: "text-purple-600",
7
+ indigo: "text-indigo-600",
8
+ gray: "text-gray-600",
9
+ };
10
+
11
+ export const methodToColor = {
12
+ get: TextColorMap.green,
13
+ post: TextColorMap.blue,
14
+ put: TextColorMap.yellow,
15
+ delete: TextColorMap.red,
16
+ patch: TextColorMap.purple,
17
+ options: TextColorMap.indigo,
18
+ head: TextColorMap.gray,
19
+ trace: TextColorMap.gray,
20
+ };
21
+
22
+ export const methodForColor = (method: string) => {
23
+ return (
24
+ methodToColor[method.toLocaleLowerCase() as keyof typeof methodToColor] ??
25
+ TextColorMap.gray
26
+ );
27
+ };
@@ -0,0 +1,32 @@
1
+ export function sanitizeMarkdownForMetatag(
2
+ description: string,
3
+ maxLength: number = 160,
4
+ ): string {
5
+ if (!description) {
6
+ return "";
7
+ }
8
+
9
+ return (
10
+ description
11
+ // Replace Markdown links [text](url) with just "text"
12
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
13
+ // Remove Markdown image syntax: ![alt](url)
14
+ .replace(/!\[.*?\]\(.*?\)/g, "")
15
+ // Remove other Markdown syntax (e.g., **bold**, _italic_, `code`)
16
+ .replace(/[_*`~]/g, "")
17
+ // Remove headings (# Heading), blockquotes (> Quote), and horizontal rules (--- or ***)
18
+ .replace(/^(?:>|\s*#+|-{3,}|\*{3,})/gm, "")
19
+ // Remove any remaining formatting characters
20
+ .replace(/[|>{}[\]]/g, "")
21
+ // Collapse multiple spaces and trim the text
22
+ .replace(/\s+/g, " ")
23
+ .trim()
24
+ // Limit to the specified maximum length
25
+ .substring(0, maxLength)
26
+ .replace(/&/g, "&amp;")
27
+ .replace(/</g, "&lt;")
28
+ .replace(/>/g, "&gt;")
29
+ .replace(/"/g, "&quot;")
30
+ .replace(/'/g, "&#039;")
31
+ );
32
+ }