docusaurus-plugin-openapi-docs 4.5.0 → 4.6.0

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.
package/src/index.ts CHANGED
@@ -123,9 +123,12 @@ export default function pluginOpenAPIDocs(
123
123
  markdownGenerators,
124
124
  downloadUrl,
125
125
  sidebarOptions,
126
+ schemasOnly,
126
127
  disableCompression,
127
128
  } = options;
128
129
 
130
+ const isSchemasOnly = schemasOnly === true;
131
+
129
132
  // Remove trailing slash before proceeding
130
133
  outputDir = outputDir.replace(/\/$/, "");
131
134
 
@@ -160,7 +163,7 @@ export default function pluginOpenAPIDocs(
160
163
  }
161
164
 
162
165
  // TODO: figure out better way to set default
163
- if (Object.keys(sidebarOptions ?? {}).length > 0) {
166
+ if (!isSchemasOnly && Object.keys(sidebarOptions ?? {}).length > 0) {
164
167
  const sidebarSlice = generateSidebarSlice(
165
168
  sidebarOptions!,
166
169
  options,
@@ -234,6 +237,9 @@ hide_send_button: true
234
237
  {{#frontMatter.show_extensions}}
235
238
  show_extensions: true
236
239
  {{/frontMatter.show_extensions}}
240
+ {{#frontMatter.mask_credentials_disabled}}
241
+ mask_credentials: false
242
+ {{/frontMatter.mask_credentials_disabled}}
237
243
  ---
238
244
 
239
245
  {{{markdown}}}
@@ -329,6 +335,9 @@ custom_edit_url: null
329
335
  }
330
336
  const markdown = pageGeneratorByType[item.type](item as any);
331
337
  item.markdown = markdown;
338
+ if (isSchemasOnly && item.type !== "schema") {
339
+ return;
340
+ }
332
341
  if (item.type === "api") {
333
342
  // opportunity to compress JSON
334
343
  // const serialize = (o: any) => {
@@ -344,9 +353,18 @@ custom_edit_url: null
344
353
  .toString("base64"));
345
354
  let infoBasePath = `${outputDir}/${item.infoId}`;
346
355
  if (docRouteBasePath) {
347
- infoBasePath = `${docRouteBasePath}/${outputDir
348
- .split(docPath!)[1]
349
- .replace(/^\/+/g, "")}/${item.infoId}`.replace(/^\/+/g, "");
356
+ // Safely extract path segment, handling cases where docPath may not be in outputDir
357
+ const outputSegment =
358
+ docPath && outputDir.includes(docPath)
359
+ ? (outputDir.split(docPath)[1]?.replace(/^\/+/g, "") ?? "")
360
+ : outputDir
361
+ .slice(outputDir.indexOf("/", 1))
362
+ .replace(/^\/+/g, "");
363
+ infoBasePath =
364
+ `${docRouteBasePath}/${outputSegment}/${item.infoId}`.replace(
365
+ /^\/+/g,
366
+ ""
367
+ );
350
368
  }
351
369
  if (item.infoId) item.infoPath = infoBasePath;
352
370
  }
@@ -482,29 +500,53 @@ custom_edit_url: null
482
500
  }
483
501
  }
484
502
 
485
- async function cleanApiDocs(options: APIOptions) {
503
+ async function cleanApiDocs(options: APIOptions, schemasOnly = false) {
486
504
  const { outputDir } = options;
487
505
  const apiDir = posixPath(path.join(siteDir, outputDir));
488
- const apiMdxFiles = await Globby(["*.api.mdx", "*.info.mdx", "*.tag.mdx"], {
489
- cwd: path.resolve(apiDir),
490
- deep: 1,
491
- });
492
- const sidebarFile = await Globby(["sidebar.js", "sidebar.ts"], {
493
- cwd: path.resolve(apiDir),
494
- deep: 1,
495
- });
496
- apiMdxFiles.map((mdx) =>
497
- fs.unlink(`${apiDir}/${mdx}`, (err) => {
498
- if (err) {
499
- console.error(
500
- chalk.red(`Cleanup failed for "${apiDir}/${mdx}"`),
501
- chalk.yellow(err)
502
- );
503
- } else {
504
- console.log(chalk.green(`Cleanup succeeded for "${apiDir}/${mdx}"`));
506
+
507
+ // When schemasOnly is true, only clean the schemas directory
508
+ if (!schemasOnly) {
509
+ const apiMdxFiles = await Globby(
510
+ ["*.api.mdx", "*.info.mdx", "*.tag.mdx"],
511
+ {
512
+ cwd: path.resolve(apiDir),
513
+ deep: 1,
505
514
  }
506
- })
507
- );
515
+ );
516
+ const sidebarFile = await Globby(["sidebar.js", "sidebar.ts"], {
517
+ cwd: path.resolve(apiDir),
518
+ deep: 1,
519
+ });
520
+ apiMdxFiles.map((mdx) =>
521
+ fs.unlink(`${apiDir}/${mdx}`, (err) => {
522
+ if (err) {
523
+ console.error(
524
+ chalk.red(`Cleanup failed for "${apiDir}/${mdx}"`),
525
+ chalk.yellow(err)
526
+ );
527
+ } else {
528
+ console.log(
529
+ chalk.green(`Cleanup succeeded for "${apiDir}/${mdx}"`)
530
+ );
531
+ }
532
+ })
533
+ );
534
+
535
+ sidebarFile.map((sidebar) =>
536
+ fs.unlink(`${apiDir}/${sidebar}`, (err) => {
537
+ if (err) {
538
+ console.error(
539
+ chalk.red(`Cleanup failed for "${apiDir}/${sidebar}"`),
540
+ chalk.yellow(err)
541
+ );
542
+ } else {
543
+ console.log(
544
+ chalk.green(`Cleanup succeeded for "${apiDir}/${sidebar}"`)
545
+ );
546
+ }
547
+ })
548
+ );
549
+ }
508
550
 
509
551
  try {
510
552
  fs.rmSync(`${apiDir}/schemas`, { recursive: true });
@@ -517,21 +559,6 @@ custom_edit_url: null
517
559
  );
518
560
  }
519
561
  }
520
-
521
- sidebarFile.map((sidebar) =>
522
- fs.unlink(`${apiDir}/${sidebar}`, (err) => {
523
- if (err) {
524
- console.error(
525
- chalk.red(`Cleanup failed for "${apiDir}/${sidebar}"`),
526
- chalk.yellow(err)
527
- );
528
- } else {
529
- console.log(
530
- chalk.green(`Cleanup succeeded for "${apiDir}/${sidebar}"`)
531
- );
532
- }
533
- })
534
- );
535
562
  }
536
563
 
537
564
  async function generateVersions(versions: object, outputDir: string) {
@@ -632,7 +659,7 @@ custom_edit_url: null
632
659
  }
633
660
  }
634
661
 
635
- async function cleanAllVersions(options: APIOptions) {
662
+ async function cleanAllVersions(options: APIOptions, schemasOnly = false) {
636
663
  const parentOptions = Object.assign({}, options);
637
664
 
638
665
  const { versions } = parentOptions as any;
@@ -640,14 +667,16 @@ custom_edit_url: null
640
667
  delete parentOptions.versions;
641
668
 
642
669
  if (versions != null && Object.keys(versions).length > 0) {
643
- await cleanVersions(parentOptions.outputDir);
670
+ if (!schemasOnly) {
671
+ await cleanVersions(parentOptions.outputDir);
672
+ }
644
673
  Object.keys(versions).forEach(async (key) => {
645
674
  const versionOptions = versions[key];
646
675
  const mergedOptions = {
647
676
  ...parentOptions,
648
677
  ...versionOptions,
649
678
  };
650
- await cleanApiDocs(mergedOptions);
679
+ await cleanApiDocs(mergedOptions, schemasOnly);
651
680
  });
652
681
  }
653
682
  }
@@ -665,10 +694,12 @@ custom_edit_url: null
665
694
  .arguments("<id>")
666
695
  .option("-p, --plugin-id <plugin>", "OpenAPI docs plugin ID.")
667
696
  .option("--all-versions", "Generate all versions.")
697
+ .option("--schemas-only", "Generate only schema docs.")
668
698
  .action(async (id, instance) => {
669
699
  const options = instance.opts();
670
700
  const pluginId = options.pluginId;
671
701
  const allVersions = options.allVersions;
702
+ const schemasOnly = options.schemasOnly;
672
703
  const pluginInstances = getPluginInstances(plugins);
673
704
  let targetConfig: any;
674
705
  let targetDocsPluginId: any;
@@ -695,6 +726,9 @@ custom_edit_url: null
695
726
  targetConfig = config;
696
727
  }
697
728
 
729
+ const withSchemaOverride = (apiOptions: APIOptions): APIOptions =>
730
+ schemasOnly ? { ...apiOptions, schemasOnly: true } : apiOptions;
731
+
698
732
  if (id === "all") {
699
733
  if (targetConfig[id]) {
700
734
  console.error(
@@ -704,12 +738,10 @@ custom_edit_url: null
704
738
  );
705
739
  } else {
706
740
  Object.keys(targetConfig).forEach(async function (key) {
707
- await generateApiDocs(targetConfig[key], targetDocsPluginId);
741
+ const apiOptions = withSchemaOverride(targetConfig[key]);
742
+ await generateApiDocs(apiOptions, targetDocsPluginId);
708
743
  if (allVersions) {
709
- await generateAllVersions(
710
- targetConfig[key],
711
- targetDocsPluginId
712
- );
744
+ await generateAllVersions(apiOptions, targetDocsPluginId);
713
745
  }
714
746
  });
715
747
  }
@@ -718,9 +750,10 @@ custom_edit_url: null
718
750
  chalk.red(`ID '${id}' does not exist in OpenAPI docs config.`)
719
751
  );
720
752
  } else {
721
- await generateApiDocs(targetConfig[id], targetDocsPluginId);
753
+ const apiOptions = withSchemaOverride(targetConfig[id]);
754
+ await generateApiDocs(apiOptions, targetDocsPluginId);
722
755
  if (allVersions) {
723
- await generateAllVersions(targetConfig[id], targetDocsPluginId);
756
+ await generateAllVersions(apiOptions, targetDocsPluginId);
724
757
  }
725
758
  }
726
759
  });
@@ -733,9 +766,11 @@ custom_edit_url: null
733
766
  .usage("<id:version>")
734
767
  .arguments("<id:version>")
735
768
  .option("-p, --plugin-id <plugin>", "OpenAPI docs plugin ID.")
769
+ .option("--schemas-only", "Generate only schema docs.")
736
770
  .action(async (id, instance) => {
737
771
  const options = instance.opts();
738
772
  const pluginId = options.pluginId;
773
+ const schemasOnly = options.schemasOnly;
739
774
  const pluginInstances = getPluginInstances(plugins);
740
775
  let targetConfig: any;
741
776
  let targetDocsPluginId: any;
@@ -764,6 +799,9 @@ custom_edit_url: null
764
799
  const [parentId, versionId] = id.split(":");
765
800
  const parentConfig = Object.assign({}, targetConfig[parentId]);
766
801
 
802
+ const withSchemaOverride = (apiOptions: APIOptions): APIOptions =>
803
+ schemasOnly ? { ...apiOptions, schemasOnly: true } : apiOptions;
804
+
767
805
  const version = parentConfig.version as string;
768
806
  const label = parentConfig.label as string;
769
807
  const baseUrl = parentConfig.baseUrl as string;
@@ -793,10 +831,10 @@ custom_edit_url: null
793
831
  await generateVersions(mergedVersions, parentConfig.outputDir);
794
832
  Object.keys(versions).forEach(async (key) => {
795
833
  const versionConfig = versions[key];
796
- const mergedConfig = {
834
+ const mergedConfig = withSchemaOverride({
797
835
  ...parentConfig,
798
836
  ...versionConfig,
799
- };
837
+ });
800
838
  await generateApiDocs(mergedConfig, targetDocsPluginId);
801
839
  });
802
840
  }
@@ -808,10 +846,10 @@ custom_edit_url: null
808
846
  );
809
847
  } else {
810
848
  const versionConfig = versions[versionId];
811
- const mergedConfig = {
849
+ const mergedConfig = withSchemaOverride({
812
850
  ...parentConfig,
813
851
  ...versionConfig,
814
- };
852
+ });
815
853
  await generateVersions(mergedVersions, parentConfig.outputDir);
816
854
  await generateApiDocs(mergedConfig, targetDocsPluginId);
817
855
  }
@@ -826,10 +864,12 @@ custom_edit_url: null
826
864
  .arguments("<id>")
827
865
  .option("-p, --plugin-id <plugin>", "OpenAPI docs plugin ID.")
828
866
  .option("--all-versions", "Clean all versions.")
867
+ .option("--schemas-only", "Clean only schema docs.")
829
868
  .action(async (id, instance) => {
830
869
  const options = instance.opts();
831
870
  const pluginId = options.pluginId;
832
871
  const allVersions = options.allVersions;
872
+ const schemasOnly = options.schemasOnly;
833
873
  const pluginInstances = getPluginInstances(plugins);
834
874
  let targetConfig: any;
835
875
  if (pluginId) {
@@ -862,16 +902,16 @@ custom_edit_url: null
862
902
  );
863
903
  } else {
864
904
  Object.keys(targetConfig).forEach(async function (key) {
865
- await cleanApiDocs(targetConfig[key]);
905
+ await cleanApiDocs(targetConfig[key], schemasOnly);
866
906
  if (allVersions) {
867
- await cleanAllVersions(targetConfig[key]);
907
+ await cleanAllVersions(targetConfig[key], schemasOnly);
868
908
  }
869
909
  });
870
910
  }
871
911
  } else {
872
- await cleanApiDocs(targetConfig[id]);
912
+ await cleanApiDocs(targetConfig[id], schemasOnly);
873
913
  if (allVersions) {
874
- await cleanAllVersions(targetConfig[id]);
914
+ await cleanAllVersions(targetConfig[id], schemasOnly);
875
915
  }
876
916
  }
877
917
  });
@@ -884,9 +924,11 @@ custom_edit_url: null
884
924
  .usage("<id:version>")
885
925
  .arguments("<id:version>")
886
926
  .option("-p, --plugin-id <plugin>", "OpenAPI docs plugin ID.")
927
+ .option("--schemas-only", "Clean only schema docs.")
887
928
  .action(async (id, instance) => {
888
929
  const options = instance.opts();
889
930
  const pluginId = options.pluginId;
931
+ const schemasOnly = options.schemasOnly;
890
932
  const pluginInstances = getPluginInstances(plugins);
891
933
  let targetConfig: any;
892
934
  if (pluginId) {
@@ -922,14 +964,16 @@ custom_edit_url: null
922
964
  "Can't use id 'all' for OpenAPI docs versions configuration key."
923
965
  );
924
966
  } else {
925
- await cleanVersions(parentConfig.outputDir);
967
+ if (!schemasOnly) {
968
+ await cleanVersions(parentConfig.outputDir);
969
+ }
926
970
  Object.keys(versions).forEach(async (key) => {
927
971
  const versionConfig = versions[key];
928
972
  const mergedConfig = {
929
973
  ...parentConfig,
930
974
  ...versionConfig,
931
975
  };
932
- await cleanApiDocs(mergedConfig);
976
+ await cleanApiDocs(mergedConfig, schemasOnly);
933
977
  });
934
978
  }
935
979
  } else {
@@ -938,7 +982,7 @@ custom_edit_url: null
938
982
  ...parentConfig,
939
983
  ...versionConfig,
940
984
  };
941
- await cleanApiDocs(mergedConfig);
985
+ await cleanApiDocs(mergedConfig, schemasOnly);
942
986
  }
943
987
  });
944
988
  },
@@ -5,14 +5,14 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  * ========================================================================== */
7
7
 
8
- import { clean, create } from "./utils";
8
+ import { create } from "./utils";
9
9
 
10
10
  export function createHeading(heading: string) {
11
11
  return [
12
12
  create(
13
13
  "Heading",
14
14
  {
15
- children: clean(heading),
15
+ children: heading,
16
16
  as: "h1",
17
17
  className: "openapi__heading",
18
18
  },
@@ -9,16 +9,14 @@ import { create } from "./utils";
9
9
 
10
10
  export function createRequestHeader(header: string) {
11
11
  return [
12
- create(
13
- "Heading",
14
- {
15
- children: header,
16
- id: header.replace(" ", "-").toLowerCase(),
17
- as: "h2",
18
- className: "openapi-tabs__heading",
19
- },
20
- { inline: true }
21
- ),
22
- `\n\n`,
12
+ create("Heading", {
13
+ id: header.replace(" ", "-").toLowerCase(),
14
+ as: "h2",
15
+ className: "openapi-tabs__heading",
16
+ children: [
17
+ `<Translate id="theme.openapi.request.title">${header}</Translate>`,
18
+ ],
19
+ }),
20
+ "\n\n",
23
21
  ];
24
22
  }
@@ -375,7 +375,8 @@ function createDetailsNode(
375
375
  () => [
376
376
  create("span", {
377
377
  className: "openapi-schema__required",
378
- children: "required",
378
+ children:
379
+ "<Translate id='theme.openapi.schemaItem.required'>required</Translate>",
379
380
  }),
380
381
  ]
381
382
  ),
@@ -530,7 +531,8 @@ function createPropertyDiscriminator(
530
531
  guard(required, () => [
531
532
  create("span", {
532
533
  className: "openapi-schema__required",
533
- children: "required",
534
+ children:
535
+ "<Translate id='theme.openapi.schemaItem.required'>required</Translate>",
534
536
  }),
535
537
  ]),
536
538
  ],
@@ -72,7 +72,8 @@ export function createApiPageMD({
72
72
  `import StatusCodes from "@theme/StatusCodes";\n`,
73
73
  `import OperationTabs from "@theme/OperationTabs";\n`,
74
74
  `import TabItem from "@theme/TabItem";\n`,
75
- `import Heading from "@theme/Heading";\n\n`,
75
+ `import Heading from "@theme/Heading";\n`,
76
+ `import Translate from "@docusaurus/Translate";\n\n`,
76
77
  createHeading(title),
77
78
  createMethodEndpoint(method, path),
78
79
  infoPath && createAuthorization(infoPath),
@@ -134,7 +135,7 @@ export function createSchemaPageMD({ schema }: SchemaPageMetadata) {
134
135
  return render([
135
136
  `import Schema from "@theme/Schema";\n`,
136
137
  `import Heading from "@theme/Heading";\n\n`,
137
- createHeading(title.replace(lessThan, "&lt;").replace(greaterThan, "&gt;")),
138
+ createHeading(title),
138
139
  createDescription(description),
139
140
  create("Schema", {
140
141
  schema: schema,
@@ -0,0 +1,17 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: Webhook Example
4
+ version: 1.0.0
5
+ paths: {}
6
+ webhooks:
7
+ order.created:
8
+ post:
9
+ requestBody:
10
+ description: example body
11
+ content:
12
+ application/json:
13
+ schema:
14
+ type: object
15
+ responses:
16
+ "200":
17
+ description: OK
@@ -0,0 +1,57 @@
1
+ /* ============================================================================
2
+ * Copyright (c) Palo Alto Networks
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * ========================================================================== */
7
+
8
+ import { sampleFromSchema } from "./createSchemaExample";
9
+ import { SchemaObject } from "./types";
10
+
11
+ describe("sampleFromSchema", () => {
12
+ describe("const support", () => {
13
+ it("should return default string value when const is not present", () => {
14
+ const schema: SchemaObject = {
15
+ type: "string",
16
+ };
17
+ const context = { type: "request" as const };
18
+
19
+ const result = sampleFromSchema(schema, context);
20
+
21
+ expect(result).toBe("string");
22
+ });
23
+
24
+ it("should return const value when const is present", () => {
25
+ const schema: SchemaObject = {
26
+ type: "string",
27
+ const: "example",
28
+ };
29
+ const context = { type: "request" as const };
30
+
31
+ const result = sampleFromSchema(schema, context);
32
+
33
+ expect(result).toBe("example");
34
+ });
35
+
36
+ it("should handle anyOf with const values", () => {
37
+ const schema: SchemaObject = {
38
+ type: "string",
39
+ anyOf: [
40
+ {
41
+ type: "string",
42
+ const: "dog",
43
+ },
44
+ {
45
+ type: "string",
46
+ const: "cat",
47
+ },
48
+ ],
49
+ };
50
+ const context = { type: "request" as const };
51
+
52
+ const result = sampleFromSchema(schema, context);
53
+
54
+ expect(result).toBe("dog");
55
+ });
56
+ });
57
+ });
@@ -91,7 +91,12 @@ function sampleFromProp(
91
91
 
92
92
  // TODO: handle discriminators
93
93
 
94
- if (prop.oneOf) {
94
+ // Check for explicit example/examples first (OAS 3.1 support)
95
+ if (prop.example !== undefined) {
96
+ obj[name] = prop.example;
97
+ } else if (prop.examples !== undefined && prop.examples.length > 0) {
98
+ obj[name] = prop.examples[0];
99
+ } else if (prop.oneOf) {
95
100
  obj[name] = sampleFromSchema(prop.oneOf[0], context);
96
101
  } else if (prop.anyOf) {
97
102
  obj[name] = sampleFromSchema(prop.anyOf[0], context);
@@ -111,12 +116,27 @@ export const sampleFromSchema = (
111
116
  try {
112
117
  // deep copy schema before processing
113
118
  let schemaCopy = JSON.parse(JSON.stringify(schema));
114
- let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy;
119
+ let {
120
+ type,
121
+ example,
122
+ examples,
123
+ allOf,
124
+ properties,
125
+ items,
126
+ oneOf,
127
+ anyOf,
128
+ const: constant,
129
+ } = schemaCopy;
115
130
 
116
131
  if (example !== undefined) {
117
132
  return example;
118
133
  }
119
134
 
135
+ // OAS 3.1 / JSON Schema: examples is an array
136
+ if (examples !== undefined && examples.length > 0) {
137
+ return examples[0];
138
+ }
139
+
120
140
  if (oneOf) {
121
141
  if (properties) {
122
142
  const combinedSchemas = merge(schemaCopy, oneOf[0]);
@@ -218,6 +238,10 @@ export const sampleFromSchema = (
218
238
  return undefined;
219
239
  }
220
240
 
241
+ if (constant) {
242
+ return constant;
243
+ }
244
+
221
245
  return primitive(schemaCopy);
222
246
  } catch (err) {
223
247
  console.error(
@@ -11,6 +11,8 @@ import path from "path";
11
11
  import { posixPath } from "@docusaurus/utils";
12
12
 
13
13
  import { readOpenapiFiles } from ".";
14
+ import { processOpenapiFile } from "./openapi";
15
+ import type { APIOptions, SidebarOptions } from "../types";
14
16
 
15
17
  // npx jest packages/docusaurus-plugin-openapi/src/openapi/openapi.test.ts --watch
16
18
 
@@ -37,4 +39,60 @@ describe("openapi", () => {
37
39
  ).toBeDefined();
38
40
  });
39
41
  });
42
+
43
+ describe("schemasOnly", () => {
44
+ it("includes schema metadata when showSchemas is disabled", async () => {
45
+ const openapiData = {
46
+ openapi: "3.0.0",
47
+ info: {
48
+ title: "Schema Only",
49
+ version: "1.0.0",
50
+ },
51
+ paths: {
52
+ "/ping": {
53
+ get: {
54
+ summary: "Ping",
55
+ responses: {
56
+ "200": {
57
+ description: "OK",
58
+ },
59
+ },
60
+ },
61
+ },
62
+ },
63
+ components: {
64
+ schemas: {
65
+ WithoutTags: {
66
+ title: "Without Tags",
67
+ type: "object",
68
+ properties: {
69
+ value: {
70
+ type: "string",
71
+ },
72
+ },
73
+ },
74
+ },
75
+ },
76
+ };
77
+
78
+ const options: APIOptions = {
79
+ specPath: "dummy", // required by the type but unused in this context
80
+ outputDir: "build",
81
+ showSchemas: false,
82
+ schemasOnly: true,
83
+ };
84
+
85
+ const sidebarOptions = {} as SidebarOptions;
86
+
87
+ const [items] = await processOpenapiFile(
88
+ openapiData as any,
89
+ options,
90
+ sidebarOptions
91
+ );
92
+
93
+ const schemaItems = items.filter((item) => item.type === "schema");
94
+ expect(schemaItems).toHaveLength(1);
95
+ expect(schemaItems[0].id).toBe("without-tags");
96
+ });
97
+ });
40
98
  });