@forsakringskassan/docs-generator 3.19.0 → 3.20.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.
@@ -2,7 +2,7 @@ import { createRequire as $createRequire } from "node:module";
2
2
 
3
3
  const require = $createRequire(import.meta.url);
4
4
 
5
- import { r as runAsWorker, f as format2, a as resolveConfig } from './vendor-i-8GIz5x.mjs';
5
+ import { r as runAsWorker, f as format2, a as resolveConfig } from './vendor-CLyw83WG.mjs';
6
6
  import 'node:url';
7
7
  import 'node:path';
8
8
  import 'fs';
@@ -54,4 +54,4 @@ async function formatCode(source, filepath) {
54
54
  runAsWorker(formatCode);
55
55
 
56
56
  export { formatCode };
57
- //# sourceMappingURL=format-code.worker-CFQ372Nb.mjs.map
57
+ //# sourceMappingURL=format-code.worker-DR9lHLhH.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"format-code.worker-CFQ372Nb.mjs","sources":["../src/utils/format-code.worker.ts"],"sourcesContent":["import { type Options, format as prettier, resolveConfig } from \"prettier\";\nimport { runAsWorker } from \"synckit\";\n\nconst configCache = new Map<string, Options | null>();\n\nasync function getConfig(filepath: string): Promise<Options | null> {\n const cached = configCache.get(filepath);\n if (cached !== undefined) {\n return cached;\n }\n const options = await resolveConfig(filepath);\n configCache.set(filepath, options);\n return options;\n}\n\n/**\n * @internal\n */\nexport async function formatCode(\n source: string,\n filepath: string,\n): Promise<string> {\n const options = await getConfig(filepath);\n const formatted = await prettier(source, { ...options, filepath });\n return formatted.trim();\n}\n\n/* eslint-disable-next-line unicorn/no-top-level-side-effects -- required for worker to function */\nrunAsWorker(formatCode);\n"],"names":["prettier"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,MAAM,WAAA,uBAAkB,GAAA,EAA4B;AAEpD,eAAe,UAAU,QAAA,EAA2C;AAChE,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,GAAA,CAAI,QAAQ,CAAA;AACvC,EAAA,IAAI,WAAW,MAAA,EAAW;AACtB,IAAA,OAAO,MAAA;AAAA,EACX;AACA,EAAA,MAAM,OAAA,GAAU,MAAM,aAAA,CAAc,QAAQ,CAAA;AAC5C,EAAA,WAAA,CAAY,GAAA,CAAI,UAAU,OAAO,CAAA;AACjC,EAAA,OAAO,OAAA;AACX;AAKA,eAAsB,UAAA,CAClB,QACA,QAAA,EACe;AACf,EAAA,MAAM,OAAA,GAAU,MAAM,SAAA,CAAU,QAAQ,CAAA;AACxC,EAAA,MAAM,SAAA,GAAY,MAAMA,OAAA,CAAS,MAAA,EAAQ,EAAE,GAAG,OAAA,EAAS,UAAU,CAAA;AACjE,EAAA,OAAO,UAAU,IAAA,EAAK;AAC1B;AAGA,WAAA,CAAY,UAAU,CAAA;;"}
1
+ {"version":3,"file":"format-code.worker-DR9lHLhH.mjs","sources":["../src/utils/format-code.worker.ts"],"sourcesContent":["import { type Options, format as prettier, resolveConfig } from \"prettier\";\nimport { runAsWorker } from \"synckit\";\n\nconst configCache = new Map<string, Options | null>();\n\nasync function getConfig(filepath: string): Promise<Options | null> {\n const cached = configCache.get(filepath);\n if (cached !== undefined) {\n return cached;\n }\n const options = await resolveConfig(filepath);\n configCache.set(filepath, options);\n return options;\n}\n\n/**\n * @internal\n */\nexport async function formatCode(\n source: string,\n filepath: string,\n): Promise<string> {\n const options = await getConfig(filepath);\n const formatted = await prettier(source, { ...options, filepath });\n return formatted.trim();\n}\n\n/* eslint-disable-next-line unicorn/no-top-level-side-effects -- required for worker to function */\nrunAsWorker(formatCode);\n"],"names":["prettier"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,MAAM,WAAA,uBAAkB,GAAA,EAA4B;AAEpD,eAAe,UAAU,QAAA,EAA2C;AAChE,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,GAAA,CAAI,QAAQ,CAAA;AACvC,EAAA,IAAI,WAAW,MAAA,EAAW;AACtB,IAAA,OAAO,MAAA;AAAA,EACX;AACA,EAAA,MAAM,OAAA,GAAU,MAAM,aAAA,CAAc,QAAQ,CAAA;AAC5C,EAAA,WAAA,CAAY,GAAA,CAAI,UAAU,OAAO,CAAA;AACjC,EAAA,OAAO,OAAA;AACX;AAKA,eAAsB,UAAA,CAClB,QACA,QAAA,EACe;AACf,EAAA,MAAM,OAAA,GAAU,MAAM,SAAA,CAAU,QAAQ,CAAA;AACxC,EAAA,MAAM,SAAA,GAAY,MAAMA,OAAA,CAAS,MAAA,EAAQ,EAAE,GAAG,OAAA,EAAS,UAAU,CAAA;AACjE,EAAA,OAAO,UAAU,IAAA,EAAK;AAC1B;AAGA,WAAA,CAAY,UAAU,CAAA;;"}
package/dist/index.d.mts CHANGED
@@ -184,6 +184,18 @@ export declare interface DocumentPartialReference {
184
184
  reference: string;
185
185
  }
186
186
 
187
+ /**
188
+ * Serializable metadata exported with extracted documentation.
189
+ *
190
+ * This interface can be augmented to describe application-specific metadata.
191
+ *
192
+ * @public
193
+ * @since %version%
194
+ */
195
+ export declare interface ExtractedDocsMetadata {
196
+ [key: string]: unknown;
197
+ }
198
+
187
199
  /**
188
200
  * Type representing the exported docs module when extracting markdown
189
201
  * documents.
@@ -198,12 +210,14 @@ export declare interface DocumentPartialReference {
198
210
  */
199
211
  export declare interface ExtractedDocsModule {
200
212
  readonly generatorVersion: string;
213
+ readonly version: string | null;
214
+ readonly metadata: ExtractedDocsMetadata | null;
201
215
  readonly basePath: string;
202
216
  readonly exampleFolders: string[];
203
217
  readonly filesPath: string;
204
218
  readonly partialsPath: string;
205
219
  readonly linksPath: string;
206
- sourceFiles(src: Partial<SourceFiles>): SourceFiles[];
220
+ sourceFiles(src?: Partial<SourceFiles>): SourceFiles[];
207
221
  }
208
222
 
209
223
  /**
@@ -245,6 +259,17 @@ export declare interface ExtractMarkdownOptions extends ProcessorOptions {
245
259
  * Folder to write docs to.
246
260
  */
247
261
  outputFolder: string;
262
+ /**
263
+ * Version written to the `version` field of the generated `index.mjs`.
264
+ */
265
+ version?: string;
266
+ /**
267
+ * Callback providing serializable metadata for the generated `index.mjs`.
268
+ *
269
+ * Called once per extraction and awaited. Missing callbacks and nullish
270
+ * results are exported as `null`.
271
+ */
272
+ metadata?: () => ExtractedDocsMetadata | undefined | null | Promise<ExtractedDocsMetadata | undefined | null>;
248
273
  }
249
274
 
250
275
  /**
package/dist/index.mjs CHANGED
@@ -3,13 +3,13 @@ import { createRequire as $createRequire } from "node:module";
3
3
  const require = $createRequire(import.meta.url);
4
4
 
5
5
  import { ApiItemKind, ApiModel } from '@microsoft/api-extractor-model';
6
- import { Z as Ze, M as MarkdownItCallable, g as fm, s as strToU8, z as zlibSync, h as strFromU8, d as dedent, i as closest, j as distance, k as isCI, m as moduleImporter, l as cliProgress, t as tinylr, w as watch, n as createInstance, o as fse, p as inter } from './vendor-i-8GIz5x.mjs';
6
+ import { Z as Ze, M as MarkdownItCallable, g as fm, s as strToU8, z as zlibSync, h as strFromU8, d as dedent, i as closest, j as distance, k as isCI, m as moduleImporter, l as cliProgress, t as tinylr, w as watch, n as createInstance, o as fse, p as inter } from './vendor-CLyw83WG.mjs';
7
7
  import path__default from 'node:path';
8
8
  import fs from 'node:fs/promises';
9
9
  import crypto from 'node:crypto';
10
10
  import path from 'node:path/posix';
11
11
  import { exec, execSync } from 'node:child_process';
12
- import { g as generateId, f as formatCode, p as parseImport, a as getFingerprint, b as parseInfostring, h as hasTag, d as findTag, e as getOutputFilePath, i as isDocumentPage, j as findDocument, k as getDocumentBody, n as normalizePath, l as htmlencode, c as createMarkdownRenderer } from './create-markdown-renderer-DrDHT9bZ.mjs';
12
+ import { g as generateId, f as formatCode, p as parseInfostring, a as getFingerprint, h as hasTag, b as parseImport, d as findTag, e as getOutputFilePath, i as isDocumentPage, j as findDocument, k as getDocumentBody, n as normalizePath, l as htmlencode, c as createMarkdownRenderer } from './create-markdown-renderer-DSov_Gkx.mjs';
13
13
  import { DocExcerpt } from '@microsoft/tsdoc';
14
14
  import pkg from '../package.json' with { type: "json" };
15
15
  import fs__default, { existsSync, readFileSync, copyFileSync } from 'node:fs';
@@ -640,108 +640,6 @@ function isDocumentPartial(doc) {
640
640
  return doc.kind === "partial";
641
641
  }
642
642
 
643
- function getExampleName(filename) {
644
- return path__default.parse(filename).name;
645
- }
646
-
647
- const vueMajor = Number(version.split(".", 2)[0]);
648
- function vueGenerator() {
649
- switch (vueMajor) {
650
- case 2:
651
- throw new Error(
652
- "Vue 2 is no longer supported, upgrade to Vue 3 or downgrade docs-generator"
653
- );
654
- case 3:
655
- return generateCode;
656
- }
657
- }
658
- function generateExample(options) {
659
- const { language, fileMatcher } = options;
660
- if (language === "import") {
661
- const parsed = parseImport(options.source);
662
- const filename = fileMatcher(parsed.filename, "when importing example");
663
- const language2 = parsed.extension;
664
- const comments = parsed.comments;
665
- const content = fs__default.readFileSync(filename, "utf8");
666
- const example = generateExample({
667
- ...options,
668
- source: content,
669
- language: language2,
670
- filename
671
- });
672
- return { ...example, comments };
673
- }
674
- switch (language) {
675
- case "vue":
676
- return generateVueExample(options);
677
- case "html":
678
- return generateStaticExample(options);
679
- default:
680
- return generateStaticExample(options);
681
- }
682
- }
683
- function generateVueExample(options) {
684
- const { filename, source, parent, setupPath, tags } = options;
685
- const fn = vueGenerator();
686
- const slug = getExampleName(filename);
687
- const fingerprint = getFingerprint(source);
688
- const { markup, sourcecode, output } = fn({
689
- filename,
690
- slug,
691
- fingerprint,
692
- code: source,
693
- setupPath
694
- });
695
- if (output) {
696
- return {
697
- source,
698
- language: options.language,
699
- comments: [],
700
- tags,
701
- markup,
702
- output,
703
- runtime: true,
704
- task: {
705
- outputFile: output,
706
- sourcecode,
707
- sourceFile: filename,
708
- parent
709
- }
710
- };
711
- }
712
- return {
713
- source: sourcecode,
714
- language: "plaintext",
715
- comments: [],
716
- tags,
717
- markup,
718
- output: null,
719
- runtime: true,
720
- task: null
721
- };
722
- }
723
- function generateStaticExample(options) {
724
- const { filename, source, language, tags } = options;
725
- const slug = getExampleName(filename);
726
- const fingerprint = getFingerprint(source);
727
- const asset = `${slug}-${fingerprint}.${language}`;
728
- const runtimeLanguages = ["html"];
729
- const runtime = runtimeLanguages.includes(language);
730
- return {
731
- source,
732
- language: options.language,
733
- comments: [],
734
- tags,
735
- markup: source,
736
- output: runtime ? asset : null,
737
- runtime,
738
- task: null
739
- };
740
- }
741
-
742
- const url = new URL("./worker--TrTGmqL.mjs", import.meta.url);
743
- const filePath = fileURLToPath(url);
744
-
745
643
  const md$6 = MarkdownItCallable();
746
644
  md$6.renderer.rules.fence = (tokens, idx, _options, env) => {
747
645
  const { collected, exampleFileMatcher } = env;
@@ -929,39 +827,46 @@ ${JSON.stringify(attributes)}
929
827
  function cmp(a, b) {
930
828
  return a[0].localeCompare(b[0]);
931
829
  }
932
- async function extractDocument(doc, docs, partials, options) {
933
- const { outputFolder } = options;
934
- const markdown = doc.body.replaceAll(
935
- /^:::\s*api(.*)\n([\s\S]+?):::/gm,
936
- (match, info, content2) => {
937
- const needle = content2.trim();
938
- const trimmedInfo = info.trim();
939
- const tags = trimmedInfo ? trimmedInfo.split(/\s+/) : [];
940
- const result = findDocument(docs, needle);
941
- if (!result.document) {
942
- return match;
943
- }
944
- const { document, kind, reference } = result;
945
- if (!isDocumentPartial(document)) {
946
- return match;
947
- }
948
- const body = getDocumentBody(document, tags, {
949
- kind,
950
- reference
951
- });
952
- const fingerprint = getFingerprint(body);
953
- const id = `partial:${document.format}:${fingerprint}`;
954
- partials.set(id, {
955
- id,
956
- format: document.format,
957
- body
958
- });
959
- return `:::api
830
+ function findApiContainer(text, cb) {
831
+ const regex = /^:::[ \t]*api(?=[ \t]|\r?\n)([^\n]*)\n([\s\S]+?):::\n/gm;
832
+ return text.replaceAll(regex, (match, info, rawContent) => {
833
+ const content = rawContent.trim();
834
+ const trimmedInfo = info.trim();
835
+ const tags = trimmedInfo ? trimmedInfo.split(/\s+/) : [];
836
+ return cb({ match, tags, content });
837
+ });
838
+ }
839
+ function onApiContainer(docs, partials, container) {
840
+ const { match, tags, content } = container;
841
+ const result = findDocument(docs, content);
842
+ if (!result.document) {
843
+ return match;
844
+ }
845
+ const { document, kind, reference } = result;
846
+ if (!isDocumentPartial(document)) {
847
+ return match;
848
+ }
849
+ const body = getDocumentBody(document, tags, {
850
+ kind,
851
+ reference
852
+ });
853
+ const fingerprint = getFingerprint(body);
854
+ const id = `partial:${document.format}:${fingerprint}`;
855
+ partials.set(id, {
856
+ id,
857
+ format: document.format,
858
+ body
859
+ });
860
+ return `:::api
960
861
  ${id}
961
862
  :::
962
863
  `;
963
- }
964
- );
864
+ }
865
+ async function extractDocument(doc, docs, partials, options) {
866
+ const { outputFolder } = options;
867
+ const markdown = findApiContainer(doc.body, (container) => {
868
+ return onApiContainer(docs, partials, container);
869
+ });
965
870
  const src = doc.fileInfo.fullPath;
966
871
  const dst = path__default.join(outputFolder, "files", doc.fileInfo.fullPath);
967
872
  const fm2 = await getFrontMatter(src);
@@ -1003,21 +908,22 @@ async function extractLinks(docs, options) {
1003
908
  await fs.mkdir(path__default.dirname(dst), { recursive: true });
1004
909
  await fs.writeFile(dst, content, "utf8");
1005
910
  }
1006
- async function writeIndex(options) {
1007
- const { outputFolder } = options;
1008
- const dst = path__default.join(outputFolder, "index.mjs");
1009
- const lines = [
911
+ function generateIndex$1(options) {
912
+ const { generatorVersion, version, metadata = null } = options;
913
+ return [
1010
914
  `import path from "node:path";`,
1011
915
  `import { frontMatterFileReader, partialFileReader } from "@forsakringskassan/docs-generator";`,
1012
916
  ``,
1013
- `export const generatorVersion = "${pkg.version}";`,
917
+ `export const generatorVersion = ${JSON.stringify(generatorVersion)};`,
918
+ `export const version = ${JSON.stringify(version)};`,
919
+ `export const metadata = ${JSON.stringify(metadata)};`,
1014
920
  `export const basePath = import.meta.dirname;`,
1015
921
  `export const exampleFolders = [path.join(basePath, "files")];`,
1016
922
  `export const filesPath = path.join(basePath, "files");`,
1017
923
  `export const partialsPath = [path.join(basePath, "partials.json")];`,
1018
924
  `export const linksPath = [path.join(basePath, "links.json")];`,
1019
925
  ``,
1020
- `export function sourceFiles(src) {`,
926
+ `export function sourceFiles(src = {}) {`,
1021
927
  ` return [`,
1022
928
  ` { `,
1023
929
  ` include: path.join(filesPath, "**/*.md"),`,
@@ -1033,27 +939,43 @@ async function writeIndex(options) {
1033
939
  ` ];`,
1034
940
  `}`
1035
941
  ];
942
+ }
943
+ async function writeIndex(options) {
944
+ const { outputFolder, version, metadata } = options;
945
+ const dst = path__default.join(outputFolder, "index.mjs");
946
+ const lines = generateIndex$1({
947
+ generatorVersion: pkg.version,
948
+ version,
949
+ metadata
950
+ });
1036
951
  await fs.writeFile(dst, lines.join("\n"), "utf8");
1037
952
  }
1038
953
  async function writeDts(options) {
1039
954
  const { outputFolder } = options;
1040
955
  const dst = path__default.join(outputFolder, "index.d.mts");
1041
956
  const lines = [
1042
- `import { SourceFiles } from "@forsakringskassan/docs-generator";`,
957
+ `import { ExtractedDocsMetadata, SourceFiles } from "@forsakringskassan/docs-generator";`,
1043
958
  ``,
1044
959
  `export declare const generatorVersion: string;`,
960
+ `export declare const version: string | null;`,
961
+ `export declare const metadata: ExtractedDocsMetadata | null;`,
1045
962
  `export declare const basePath: string;`,
1046
963
  `export declare const exampleFolders: string[];`,
1047
964
  `export declare const filesPath: string;`,
1048
965
  `export declare const partialsPath: string;`,
1049
966
  `export declare const linksPath: string;`,
1050
967
  ``,
1051
- `export declare function sourceFiles(src: Partial<SourceFiles>): SourceFiles[];`
968
+ `export declare function sourceFiles(this: void, src?: Partial<SourceFiles>): SourceFiles[];`
1052
969
  ];
1053
970
  await fs.writeFile(dst, lines.join("\n"), "utf8");
1054
971
  }
1055
972
  function extractMarkdownProcessor(options) {
1056
- const { enabled = true, outputFolder } = options;
973
+ const {
974
+ enabled = true,
975
+ outputFolder,
976
+ version = null,
977
+ metadata: metadataFn
978
+ } = options;
1057
979
  return {
1058
980
  after: "render",
1059
981
  name: "extract-markdown-processor",
@@ -1083,12 +1005,119 @@ function extractMarkdownProcessor(options) {
1083
1005
  return extractExamples(page, { outputFolder });
1084
1006
  });
1085
1007
  await Promise.all(resources);
1086
- await writeIndex({ outputFolder });
1008
+ const metadata = await metadataFn?.() ?? null;
1009
+ await writeIndex({
1010
+ outputFolder,
1011
+ version,
1012
+ metadata
1013
+ });
1087
1014
  await writeDts({ outputFolder });
1088
1015
  }
1089
1016
  };
1090
1017
  }
1091
1018
 
1019
+ function getExampleName(filename) {
1020
+ return path__default.parse(filename).name;
1021
+ }
1022
+
1023
+ const vueMajor = Number(version.split(".", 2)[0]);
1024
+ function vueGenerator() {
1025
+ switch (vueMajor) {
1026
+ case 2:
1027
+ throw new Error(
1028
+ "Vue 2 is no longer supported, upgrade to Vue 3 or downgrade docs-generator"
1029
+ );
1030
+ case 3:
1031
+ return generateCode;
1032
+ }
1033
+ }
1034
+ function generateExample(options) {
1035
+ const { language, fileMatcher } = options;
1036
+ if (language === "import") {
1037
+ const parsed = parseImport(options.source);
1038
+ const filename = fileMatcher(parsed.filename, "when importing example");
1039
+ const language2 = parsed.extension;
1040
+ const comments = parsed.comments;
1041
+ const content = fs__default.readFileSync(filename, "utf8");
1042
+ const example = generateExample({
1043
+ ...options,
1044
+ source: content,
1045
+ language: language2,
1046
+ filename
1047
+ });
1048
+ return { ...example, comments };
1049
+ }
1050
+ switch (language) {
1051
+ case "vue":
1052
+ return generateVueExample(options);
1053
+ case "html":
1054
+ return generateStaticExample(options);
1055
+ default:
1056
+ return generateStaticExample(options);
1057
+ }
1058
+ }
1059
+ function generateVueExample(options) {
1060
+ const { filename, source, parent, setupPath, tags } = options;
1061
+ const fn = vueGenerator();
1062
+ const slug = getExampleName(filename);
1063
+ const fingerprint = getFingerprint(source);
1064
+ const { markup, sourcecode, output } = fn({
1065
+ filename,
1066
+ slug,
1067
+ fingerprint,
1068
+ code: source,
1069
+ setupPath
1070
+ });
1071
+ if (output) {
1072
+ return {
1073
+ source,
1074
+ language: options.language,
1075
+ comments: [],
1076
+ tags,
1077
+ markup,
1078
+ output,
1079
+ runtime: true,
1080
+ task: {
1081
+ outputFile: output,
1082
+ sourcecode,
1083
+ sourceFile: filename,
1084
+ parent
1085
+ }
1086
+ };
1087
+ }
1088
+ return {
1089
+ source: sourcecode,
1090
+ language: "plaintext",
1091
+ comments: [],
1092
+ tags,
1093
+ markup,
1094
+ output: null,
1095
+ runtime: true,
1096
+ task: null
1097
+ };
1098
+ }
1099
+ function generateStaticExample(options) {
1100
+ const { filename, source, language, tags } = options;
1101
+ const slug = getExampleName(filename);
1102
+ const fingerprint = getFingerprint(source);
1103
+ const asset = `${slug}-${fingerprint}.${language}`;
1104
+ const runtimeLanguages = ["html"];
1105
+ const runtime = runtimeLanguages.includes(language);
1106
+ return {
1107
+ source,
1108
+ language: options.language,
1109
+ comments: [],
1110
+ tags,
1111
+ markup: source,
1112
+ output: runtime ? asset : null,
1113
+ runtime,
1114
+ task: null
1115
+ };
1116
+ }
1117
+
1118
+ const url = new URL("./worker-DfUDllYf.mjs", import.meta.url);
1119
+ const filePath = fileURLToPath(url);
1120
+
1092
1121
  const md$5 = MarkdownItCallable();
1093
1122
  md$5.renderer.rules.fence = (tokens, idx, _options, rawEnv) => {
1094
1123
  const { collected } = rawEnv;
@@ -2138,6 +2167,13 @@ function getComponent(attrs) {
2138
2167
  }
2139
2168
  return toArray$1(attrs.component).map(normalizeComponent);
2140
2169
  }
2170
+ function normalizeBody(text) {
2171
+ text = text.replaceAll("\r\n", "\n");
2172
+ if (!text.endsWith("\n")) {
2173
+ text += "\n";
2174
+ }
2175
+ return text;
2176
+ }
2141
2177
  function parseFile$2(filePath, basePath, content) {
2142
2178
  const relative = basePath ? path__default.relative(basePath, normalizePath(filePath)) : normalizePath(filePath);
2143
2179
  const parsed = path__default.parse(relative);
@@ -2169,7 +2205,7 @@ function parseFile$2(filePath, basePath, content) {
2169
2205
  },
2170
2206
  sortorder: attributes.sortorder ?? Infinity
2171
2207
  },
2172
- body: blocks.body,
2208
+ body: normalizeBody(blocks.body),
2173
2209
  outline,
2174
2210
  format: "markdown",
2175
2211
  tags: [],