mdat 2.0.1 → 2.2.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/dist/.DS_Store CHANGED
Binary file
package/dist/bin/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import picocolors from "picocolors";
3
3
  import prettyMilliseconds from "pretty-ms";
4
- import { getMdatReports, getSoleRule, mdatClean, mdatExpand, mdatSplit, reporterMdat, rulesSchema, setLogger } from "remark-mdat";
4
+ import { getMdatReports, getSoleRule, mdatCollapse, mdatDiff, mdatExpand, mdatSplit, mdatStrip, reporterMdat, rulesSchema, setLogger } from "remark-mdat";
5
+ import { remark } from "remark";
6
+ import remarkGfm from "remark-gfm";
5
7
  import { cosmiconfig, defaultLoaders } from "cosmiconfig";
6
8
  import { TypeScriptLoader } from "cosmiconfig-typescript-loader";
7
9
  import fs from "node:fs/promises";
@@ -17,8 +19,6 @@ import { promisify } from "node:util";
17
19
  import { brotliCompress, gzip } from "node:zlib";
18
20
  import prettyBytes from "pretty-bytes";
19
21
  import { toc } from "mdast-util-toc";
20
- import { remark } from "remark";
21
- import remarkGfm from "remark-gfm";
22
22
  import { read, write } from "to-vfile";
23
23
  import "vfile";
24
24
  import { Configuration } from "unified-engine";
@@ -42,7 +42,7 @@ function deepMergeDefined(...objects) {
42
42
  //#endregion
43
43
  //#region package.json
44
44
  var name = "mdat";
45
- var version = "2.0.1";
45
+ var version = "2.2.0";
46
46
 
47
47
  //#endregion
48
48
  //#region src/lib/log.ts
@@ -603,7 +603,7 @@ async function formatWithPrettier(content, filePath) {
603
603
  if (config === void 0 && !configCache.has(configKey)) {
604
604
  config = await cachedPrettier.resolveConfig(filePath ?? process.cwd());
605
605
  configCache.set(configKey, config);
606
- if (config) log.debug(`Using Prettier config from "${config.filepath}" for "${filePath ?? process.cwd()}"`);
606
+ if (config) log.debug(`Resolved Prettier config for "${configKey}"`);
607
607
  }
608
608
  return cachedPrettier.format(content, {
609
609
  ...config,
@@ -662,7 +662,8 @@ async function findReadme() {
662
662
  }
663
663
  /**
664
664
  * Searches up for a readme.md file.
665
- * @throws {Error} if no readme is found
665
+ *
666
+ * @throws {Error} If no readme is found
666
667
  */
667
668
  async function findReadmeThrows() {
668
669
  const readme = await findReadme();
@@ -740,17 +741,26 @@ function getExpandProcessor(config, ambientRemarkConfig) {
740
741
  emphasis: "_"
741
742
  } }).use(remarkGfm).use(ambientRemarkConfig).use(() => async function(tree, file) {
742
743
  mdatSplit(tree, file);
743
- mdatClean(tree, file);
744
+ mdatCollapse(tree, file);
744
745
  await mdatExpand(tree, file, config);
745
746
  });
746
747
  }
747
- function getCleanProcessor(_config, ambientRemarkConfig) {
748
+ function getCollapseProcessor(_config, ambientRemarkConfig) {
748
749
  return remark().use({ settings: {
749
750
  bullet: "-",
750
751
  emphasis: "_"
751
752
  } }).use(remarkGfm).use(ambientRemarkConfig).use(() => function(tree, file) {
752
753
  mdatSplit(tree, file);
753
- mdatClean(tree, file);
754
+ mdatCollapse(tree, file);
755
+ });
756
+ }
757
+ function getStripProcessor(_config, ambientRemarkConfig) {
758
+ return remark().use({ settings: {
759
+ bullet: "-",
760
+ emphasis: "_"
761
+ } }).use(remarkGfm).use(ambientRemarkConfig).use(() => function(tree, file) {
762
+ mdatSplit(tree, file);
763
+ mdatStrip(tree, file);
754
764
  });
755
765
  }
756
766
 
@@ -874,9 +884,8 @@ async function createReadme(options) {
874
884
  return readmePath;
875
885
  }
876
886
  function getTemplateForConfig(templateKey, compound) {
877
- const templateString = templates_default[templateKey].content[compound ? "compound" : "explicit"];
878
- if (templateString === void 0 || templateString === "") throw new Error(`No template found for "${templateKey}"`);
879
- return templateString;
887
+ if (!(templateKey in templates_default)) throw new Error(`Unknown template "${templateKey}". Available templates: ${Object.keys(templates_default).join(", ")}`);
888
+ return templates_default[templateKey].content[compound ? "compound" : "explicit"];
880
889
  }
881
890
  function getTemplateOptions() {
882
891
  return Object.entries(templates_default).map(([key, value]) => ({
@@ -889,10 +898,11 @@ function getTemplateOptions() {
889
898
  //#endregion
890
899
  //#region src/lib/api.ts
891
900
  /**
892
- * Expand MDAT comments in one or more Markdown files.
893
- * If no files are provided, auto-finds the closest readme.md.
894
- * Writing is the responsibility of the caller (e.g. via `await write(result)`)
895
- * @returns an array of VFiles
901
+ * Expand MDAT comments in one or more Markdown files. If no files are provided,
902
+ * auto-finds the closest readme.md. Writing is the responsibility of the caller
903
+ * (e.g. via `await write(result)`)
904
+ *
905
+ * @returns An array of VFiles
896
906
  */
897
907
  async function expand(files, name, output, config, options) {
898
908
  files ??= await findReadmeThrows();
@@ -901,21 +911,37 @@ async function expand(files, name, output, config, options) {
901
911
  return results;
902
912
  }
903
913
  /**
904
- * Collapse MDAT comments in one or more Markdown files.
905
- * If no files are provided, auto-finds the closest readme.md.
906
- * Writing is the responsibility of the caller (e.g. via `await write(result)`)
907
- * @returns an array of VFiles
914
+ * Collapse MDAT comments in one or more Markdown files. If no files are
915
+ * provided, auto-finds the closest readme.md. Writing is the responsibility of
916
+ * the caller (e.g. via `await write(result)`)
917
+ *
918
+ * @returns An array of VFiles
908
919
  */
909
920
  async function collapse(files, name, output, config, options) {
910
921
  files ??= await findReadmeThrows();
911
- const results = await processFiles(files, loadConfig, getCleanProcessor, name, output, config);
922
+ const results = await processFiles(files, loadConfig, getCollapseProcessor, name, output, config);
912
923
  if (options?.format) await formatResults(results);
913
924
  return results;
914
925
  }
915
926
  /**
916
- * Dry-run expand and compare with file on disk.
917
- * If no files are provided, auto-finds the closest readme.md.
918
- * @returns per-file sync status and expanded VFiles
927
+ * Strips MDAT comments in one or more Markdown files without touching other
928
+ * content. Does _not_ automatically expand Mdat content before stripping the
929
+ * tags. If no files are provided, auto-finds the closest readme.md. Writing is
930
+ * the responsibility of the caller (e.g. via `await write(result)`)
931
+ *
932
+ * @returns An array of VFiles
933
+ */
934
+ async function strip(files, name, output, config, options) {
935
+ files ??= await findReadmeThrows();
936
+ const results = await processFiles(files, loadConfig, getStripProcessor, name, output, config);
937
+ if (options?.format) await formatResults(results);
938
+ return results;
939
+ }
940
+ /**
941
+ * Dry-run expand and compare with file on disk. If no files are provided,
942
+ * auto-finds the closest readme.md.
943
+ *
944
+ * @returns Per-file sync status and expanded VFiles
919
945
  */
920
946
  async function check(files, config, options) {
921
947
  files ??= await findReadmeThrows();
@@ -923,10 +949,26 @@ async function check(files, config, options) {
923
949
  const resolvedFiles = Array.isArray(files) ? files : [files];
924
950
  const [originals, results] = await Promise.all([Promise.all(resolvedFiles.map(async (f) => read(f))), processFiles(files, loadConfig, getExpandProcessor, void 0, void 0, config)]);
925
951
  if (options?.format) await formatResults(results);
926
- return results.map((result, i) => ({
927
- inSync: originals[i].toString().replaceAll("\r\n", "\n") === result.toString(),
952
+ return results.map((result, i) => compareWithDiff(originals[i], result, options));
953
+ }
954
+ function compareWithDiff(original, result, options) {
955
+ const inSync = original.toString().replaceAll("\r\n", "\n") === result.toString();
956
+ if (!inSync) {
957
+ const parser = remark().use(remarkGfm);
958
+ const originalTree = parser.parse(original.toString());
959
+ mdatSplit(originalTree, original);
960
+ const expandedTree = parser.parse(result.toString());
961
+ mdatSplit(expandedTree, result);
962
+ const diffResults = mdatDiff(originalTree, original, expandedTree, result);
963
+ if (options?.format && diffResults.every((r) => r.status === "ok")) {
964
+ const message = result.message("Formatting differences outside mdat tags", { source: "diff" });
965
+ message.fatal = false;
966
+ }
967
+ }
968
+ return {
969
+ inSync,
928
970
  result
929
- }));
971
+ };
930
972
  }
931
973
  async function formatResults(results) {
932
974
  for (const file of results) file.value = await formatWithPrettier(file.toString(), file.path || void 0);
@@ -1041,6 +1083,18 @@ try {
1041
1083
  reporterMdat(results);
1042
1084
  log.debug(`Collapsed comments in ${prettyMilliseconds(performance.now() - startTime)}.`);
1043
1085
  process.exitCode = getExitCode(results);
1086
+ }).command("strip [files..] [options]", "Strip MDAT comments while preserving expanded content. If no files are provided, the closest readme.md is stripped.", (yargs) => yargs.positional(...filesPositional).option(outputOption).option(nameOption).option(printOption).option(formatOption), async ({ files, format, name, output, print }) => {
1087
+ logConflicts({
1088
+ name,
1089
+ output,
1090
+ print
1091
+ });
1092
+ const results = await strip(files, name, output, void 0, { format });
1093
+ for (const file of results) if (print) process.stdout.write(file.toString());
1094
+ else await write(file);
1095
+ reporterMdat(results);
1096
+ log.debug(`Stripped comments in ${prettyMilliseconds(performance.now() - startTime)}.`);
1097
+ process.exitCode = getExitCode(results);
1044
1098
  }).command("check [files..] [options]", "Check if MDAT placeholder comments are up to date. Exits with code 1 if any files have stale or unexpanded content.", (yargs) => yargs.positional(...filesPositional).option(configOption).option(formatOption), async ({ config, files, format }) => {
1045
1099
  const results = await check(files, collectConfig(config), { format });
1046
1100
  let allInSync = true;
@@ -1049,6 +1103,7 @@ try {
1049
1103
  if (inSync) log.debug(`${picocolors.green("Up to date")}: ${filePath}`);
1050
1104
  else {
1051
1105
  log.warn(`${picocolors.red("Stale content")}: ${filePath}`);
1106
+ reporterMdat([result]);
1052
1107
  allInSync = false;
1053
1108
  }
1054
1109
  }
@@ -67,10 +67,11 @@ declare function createReadme(options?: Partial<MdatReadmeCreateOptions>): Promi
67
67
  //#endregion
68
68
  //#region src/lib/api.d.ts
69
69
  /**
70
- * Expand MDAT comments in one or more Markdown files.
71
- * If no files are provided, auto-finds the closest readme.md.
72
- * Writing is the responsibility of the caller (e.g. via `await write(result)`)
73
- * @returns an array of VFiles
70
+ * Expand MDAT comments in one or more Markdown files. If no files are provided,
71
+ * auto-finds the closest readme.md. Writing is the responsibility of the caller
72
+ * (e.g. via `await write(result)`)
73
+ *
74
+ * @returns An array of VFiles
74
75
  */
75
76
  declare function expand(files?: string | string[], name?: string, output?: string, config?: ConfigToLoad, options?: {
76
77
  format?: boolean;
@@ -82,10 +83,11 @@ declare function expandString(markdown: string, config?: ConfigToLoad, options?:
82
83
  format?: boolean;
83
84
  }): Promise<VFile>;
84
85
  /**
85
- * Collapse MDAT comments in one or more Markdown files.
86
- * If no files are provided, auto-finds the closest readme.md.
87
- * Writing is the responsibility of the caller (e.g. via `await write(result)`)
88
- * @returns an array of VFiles
86
+ * Collapse MDAT comments in one or more Markdown files. If no files are
87
+ * provided, auto-finds the closest readme.md. Writing is the responsibility of
88
+ * the caller (e.g. via `await write(result)`)
89
+ *
90
+ * @returns An array of VFiles
89
91
  */
90
92
  declare function collapse(files?: string | string[], name?: string, output?: string, config?: ConfigToLoad, options?: {
91
93
  format?: boolean;
@@ -97,9 +99,27 @@ declare function collapseString(markdown: string, config?: ConfigToLoad, options
97
99
  format?: boolean;
98
100
  }): Promise<VFile>;
99
101
  /**
100
- * Dry-run expand and compare with file on disk.
101
- * If no files are provided, auto-finds the closest readme.md.
102
- * @returns per-file sync status and expanded VFiles
102
+ * Strips MDAT comments in one or more Markdown files without touching other
103
+ * content. Does _not_ automatically expand Mdat content before stripping the
104
+ * tags. If no files are provided, auto-finds the closest readme.md. Writing is
105
+ * the responsibility of the caller (e.g. via `await write(result)`)
106
+ *
107
+ * @returns An array of VFiles
108
+ */
109
+ declare function strip(files?: string | string[], name?: string, output?: string, config?: ConfigToLoad, options?: {
110
+ format?: boolean;
111
+ }): Promise<VFile[]>;
112
+ /**
113
+ * Strip MDAT comments from a Markdown string.
114
+ */
115
+ declare function stripString(markdown: string, config?: ConfigToLoad, options?: {
116
+ format?: boolean;
117
+ }): Promise<VFile>;
118
+ /**
119
+ * Dry-run expand and compare with file on disk. If no files are provided,
120
+ * auto-finds the closest readme.md.
121
+ *
122
+ * @returns Per-file sync status and expanded VFiles
103
123
  */
104
124
  declare function check(files?: string | string[], config?: ConfigToLoad, options?: {
105
125
  format?: boolean;
@@ -107,6 +127,16 @@ declare function check(files?: string | string[], config?: ConfigToLoad, options
107
127
  inSync: boolean;
108
128
  result: VFile;
109
129
  }>>;
130
+ /**
131
+ * Check if MDAT comments in a Markdown string are up to date by expanding and
132
+ * comparing per-tag.
133
+ */
134
+ declare function checkString(markdown: string, config?: ConfigToLoad, options?: {
135
+ format?: boolean;
136
+ }): Promise<{
137
+ inSync: boolean;
138
+ result: VFile;
139
+ }>;
110
140
  //#endregion
111
141
  //#region src/lib/context.d.ts
112
142
  /**
@@ -160,4 +190,4 @@ declare function resetMetadataCaches(): void;
160
190
  */
161
191
  declare function setLogger(logger?: ILogBasic | ILogLayer): void;
162
192
  //#endregion
163
- export { type Config, type ReadmeMetadata, type Rule, check, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, resetMetadataCaches, setLogger };
193
+ export { type Config, type ReadmeMetadata, type Rule, check, checkString, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, resetMetadataCaches, setLogger, strip, stripString };
package/dist/lib/index.js CHANGED
@@ -1,10 +1,12 @@
1
+ import { remark } from "remark";
2
+ import remarkGfm from "remark-gfm";
3
+ import { getSoleRule, mdatCollapse, mdatDiff, mdatExpand, mdatSplit, mdatStrip, rulesSchema, setLogger as setLogger$1 } from "remark-mdat";
1
4
  import { cosmiconfig, defaultLoaders } from "cosmiconfig";
2
5
  import { TypeScriptLoader } from "cosmiconfig-typescript-loader";
3
6
  import fs from "node:fs/promises";
4
7
  import path from "node:path";
5
8
  import picocolors from "picocolors";
6
9
  import plur from "plur";
7
- import { getSoleRule, mdatClean, mdatExpand, mdatSplit, rulesSchema, setLogger as setLogger$1 } from "remark-mdat";
8
10
  import { deepmerge } from "deepmerge-ts";
9
11
  import { createLogger, getChildLogger, injectionHelper } from "lognow";
10
12
  import { defineTemplate, getMetadata, helpers, setLogger as setLogger$2, templates } from "metascope";
@@ -15,8 +17,6 @@ import { promisify } from "node:util";
15
17
  import { brotliCompress, gzip } from "node:zlib";
16
18
  import prettyBytes from "pretty-bytes";
17
19
  import { toc } from "mdast-util-toc";
18
- import { remark } from "remark";
19
- import remarkGfm from "remark-gfm";
20
20
  import { read, write } from "to-vfile";
21
21
  import { VFile } from "vfile";
22
22
  import { Configuration } from "unified-engine";
@@ -124,7 +124,8 @@ const GIT_PREFIX_REGEX = /^git\+/;
124
124
  const GIT_SUFFIX_REGEX = /\.git$/;
125
125
  const TRAILING_SLASH_REGEX = /\/$/;
126
126
  /**
127
- * Reset
127
+ * Reset cached context metadata. Call between tests or when the underlying
128
+ * project files may have changed on disk.
128
129
  *
129
130
  * @public
130
131
  */
@@ -163,7 +164,8 @@ async function getReadmeMetadata() {
163
164
  return readmeMetadata;
164
165
  }
165
166
  /**
166
- * Reset
167
+ * Reset cached readme metadata. Call between tests or when the underlying
168
+ * project files may have changed on disk.
167
169
  *
168
170
  * @public
169
171
  */
@@ -609,7 +611,7 @@ async function formatWithPrettier(content, filePath) {
609
611
  if (config === void 0 && !configCache.has(configKey)) {
610
612
  config = await cachedPrettier.resolveConfig(filePath ?? process.cwd());
611
613
  configCache.set(configKey, config);
612
- if (config) log.debug(`Using Prettier config from "${config.filepath}" for "${filePath ?? process.cwd()}"`);
614
+ if (config) log.debug(`Resolved Prettier config for "${configKey}"`);
613
615
  }
614
616
  return cachedPrettier.format(content, {
615
617
  ...config,
@@ -667,7 +669,8 @@ async function findReadme() {
667
669
  }
668
670
  /**
669
671
  * Searches up for a readme.md file.
670
- * @throws {Error} if no readme is found
672
+ *
673
+ * @throws {Error} If no readme is found
671
674
  */
672
675
  async function findReadmeThrows() {
673
676
  const readme = await findReadme();
@@ -748,17 +751,26 @@ function getExpandProcessor(config, ambientRemarkConfig) {
748
751
  emphasis: "_"
749
752
  } }).use(remarkGfm).use(ambientRemarkConfig).use(() => async function(tree, file) {
750
753
  mdatSplit(tree, file);
751
- mdatClean(tree, file);
754
+ mdatCollapse(tree, file);
752
755
  await mdatExpand(tree, file, config);
753
756
  });
754
757
  }
755
- function getCleanProcessor(_config, ambientRemarkConfig) {
758
+ function getCollapseProcessor(_config, ambientRemarkConfig) {
756
759
  return remark().use({ settings: {
757
760
  bullet: "-",
758
761
  emphasis: "_"
759
762
  } }).use(remarkGfm).use(ambientRemarkConfig).use(() => function(tree, file) {
760
763
  mdatSplit(tree, file);
761
- mdatClean(tree, file);
764
+ mdatCollapse(tree, file);
765
+ });
766
+ }
767
+ function getStripProcessor(_config, ambientRemarkConfig) {
768
+ return remark().use({ settings: {
769
+ bullet: "-",
770
+ emphasis: "_"
771
+ } }).use(remarkGfm).use(ambientRemarkConfig).use(() => function(tree, file) {
772
+ mdatSplit(tree, file);
773
+ mdatStrip(tree, file);
762
774
  });
763
775
  }
764
776
  //#endregion
@@ -874,9 +886,8 @@ async function createReadme(options) {
874
886
  return readmePath;
875
887
  }
876
888
  function getTemplateForConfig(templateKey, compound) {
877
- const templateString = templates_default[templateKey].content[compound ? "compound" : "explicit"];
878
- if (templateString === void 0 || templateString === "") throw new Error(`No template found for "${templateKey}"`);
879
- return templateString;
889
+ if (!(templateKey in templates_default)) throw new Error(`Unknown template "${templateKey}". Available templates: ${Object.keys(templates_default).join(", ")}`);
890
+ return templates_default[templateKey].content[compound ? "compound" : "explicit"];
880
891
  }
881
892
  function getTemplateOptions() {
882
893
  return Object.entries(templates_default).map(([key, value]) => ({
@@ -888,10 +899,11 @@ function getTemplateOptions() {
888
899
  //#endregion
889
900
  //#region src/lib/api.ts
890
901
  /**
891
- * Expand MDAT comments in one or more Markdown files.
892
- * If no files are provided, auto-finds the closest readme.md.
893
- * Writing is the responsibility of the caller (e.g. via `await write(result)`)
894
- * @returns an array of VFiles
902
+ * Expand MDAT comments in one or more Markdown files. If no files are provided,
903
+ * auto-finds the closest readme.md. Writing is the responsibility of the caller
904
+ * (e.g. via `await write(result)`)
905
+ *
906
+ * @returns An array of VFiles
895
907
  */
896
908
  async function expand(files, name, output, config, options) {
897
909
  files ??= await findReadmeThrows();
@@ -908,14 +920,15 @@ async function expandString(markdown, config, options) {
908
920
  return result;
909
921
  }
910
922
  /**
911
- * Collapse MDAT comments in one or more Markdown files.
912
- * If no files are provided, auto-finds the closest readme.md.
913
- * Writing is the responsibility of the caller (e.g. via `await write(result)`)
914
- * @returns an array of VFiles
923
+ * Collapse MDAT comments in one or more Markdown files. If no files are
924
+ * provided, auto-finds the closest readme.md. Writing is the responsibility of
925
+ * the caller (e.g. via `await write(result)`)
926
+ *
927
+ * @returns An array of VFiles
915
928
  */
916
929
  async function collapse(files, name, output, config, options) {
917
930
  files ??= await findReadmeThrows();
918
- const results = await processFiles(files, loadConfig, getCleanProcessor, name, output, config);
931
+ const results = await processFiles(files, loadConfig, getCollapseProcessor, name, output, config);
919
932
  if (options?.format) await formatResults(results);
920
933
  return results;
921
934
  }
@@ -923,14 +936,37 @@ async function collapse(files, name, output, config, options) {
923
936
  * Collapse MDAT comments in a Markdown string.
924
937
  */
925
938
  async function collapseString(markdown, config, options) {
926
- const result = await processString(markdown, loadConfig, getCleanProcessor, config);
939
+ const result = await processString(markdown, loadConfig, getCollapseProcessor, config);
927
940
  if (options?.format) await formatResults([result]);
928
941
  return result;
929
942
  }
930
943
  /**
931
- * Dry-run expand and compare with file on disk.
932
- * If no files are provided, auto-finds the closest readme.md.
933
- * @returns per-file sync status and expanded VFiles
944
+ * Strips MDAT comments in one or more Markdown files without touching other
945
+ * content. Does _not_ automatically expand Mdat content before stripping the
946
+ * tags. If no files are provided, auto-finds the closest readme.md. Writing is
947
+ * the responsibility of the caller (e.g. via `await write(result)`)
948
+ *
949
+ * @returns An array of VFiles
950
+ */
951
+ async function strip(files, name, output, config, options) {
952
+ files ??= await findReadmeThrows();
953
+ const results = await processFiles(files, loadConfig, getStripProcessor, name, output, config);
954
+ if (options?.format) await formatResults(results);
955
+ return results;
956
+ }
957
+ /**
958
+ * Strip MDAT comments from a Markdown string.
959
+ */
960
+ async function stripString(markdown, config, options) {
961
+ const result = await processString(markdown, loadConfig, getStripProcessor, config);
962
+ if (options?.format) await formatResults([result]);
963
+ return result;
964
+ }
965
+ /**
966
+ * Dry-run expand and compare with file on disk. If no files are provided,
967
+ * auto-finds the closest readme.md.
968
+ *
969
+ * @returns Per-file sync status and expanded VFiles
934
970
  */
935
971
  async function check(files, config, options) {
936
972
  files ??= await findReadmeThrows();
@@ -938,13 +974,40 @@ async function check(files, config, options) {
938
974
  const resolvedFiles = Array.isArray(files) ? files : [files];
939
975
  const [originals, results] = await Promise.all([Promise.all(resolvedFiles.map(async (f) => read(f))), processFiles(files, loadConfig, getExpandProcessor, void 0, void 0, config)]);
940
976
  if (options?.format) await formatResults(results);
941
- return results.map((result, i) => ({
942
- inSync: originals[i].toString().replaceAll("\r\n", "\n") === result.toString(),
977
+ return results.map((result, i) => compareWithDiff(originals[i], result, options));
978
+ }
979
+ /**
980
+ * Check if MDAT comments in a Markdown string are up to date by expanding and
981
+ * comparing per-tag.
982
+ */
983
+ async function checkString(markdown, config, options) {
984
+ const { VFile: VF } = await import("vfile");
985
+ const original = new VF(markdown);
986
+ const result = await processString(markdown, loadConfig, getExpandProcessor, config);
987
+ if (options?.format) await formatResults([result]);
988
+ return compareWithDiff(original, result, options);
989
+ }
990
+ function compareWithDiff(original, result, options) {
991
+ const inSync = original.toString().replaceAll("\r\n", "\n") === result.toString();
992
+ if (!inSync) {
993
+ const parser = remark().use(remarkGfm);
994
+ const originalTree = parser.parse(original.toString());
995
+ mdatSplit(originalTree, original);
996
+ const expandedTree = parser.parse(result.toString());
997
+ mdatSplit(expandedTree, result);
998
+ const diffResults = mdatDiff(originalTree, original, expandedTree, result);
999
+ if (options?.format && diffResults.every((r) => r.status === "ok")) {
1000
+ const message = result.message("Formatting differences outside mdat tags", { source: "diff" });
1001
+ message.fatal = false;
1002
+ }
1003
+ }
1004
+ return {
1005
+ inSync,
943
1006
  result
944
- }));
1007
+ };
945
1008
  }
946
1009
  async function formatResults(results) {
947
1010
  for (const file of results) file.value = await formatWithPrettier(file.toString(), file.path || void 0);
948
1011
  }
949
1012
  //#endregion
950
- export { check, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, resetMetadataCaches, setLogger };
1013
+ export { check, checkString, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, resetMetadataCaches, setLogger, strip, stripString };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdat",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "CLI tool and TypeScript library implementing the Markdown Autophagic Template (MDAT) system. MDAT lets you use comments as dynamic content templates in Markdown files, making it easy to generate and update readme boilerplate.",
5
5
  "keywords": [
6
6
  "mdat",
@@ -50,7 +50,7 @@
50
50
  "globby": "^16.2.0",
51
51
  "lognow": "^0.6.0",
52
52
  "mdast-util-toc": "^7.1.0",
53
- "metascope": "^0.5.0",
53
+ "metascope": "^0.6.0",
54
54
  "path-type": "^6.0.0",
55
55
  "picocolors": "^1.1.1",
56
56
  "plur": "^6.0.0",
@@ -58,7 +58,7 @@
58
58
  "pretty-ms": "^9.3.0",
59
59
  "remark": "^15.0.1",
60
60
  "remark-gfm": "^4.0.1",
61
- "remark-mdat": "^2.0.2",
61
+ "remark-mdat": "^2.2.0",
62
62
  "to-vfile": "^8.0.0",
63
63
  "type-fest": "^5.5.0",
64
64
  "unified-engine": "^11.2.2",
@@ -69,16 +69,17 @@
69
69
  },
70
70
  "devDependencies": {
71
71
  "@arethetypeswrong/core": "^0.18.2",
72
- "@kitschpatrol/shared-config": "^7.0.0",
72
+ "@kitschpatrol/shared-config": "^7.0.1",
73
73
  "@types/mdast": "^4.0.4",
74
74
  "@types/node": "~22.17.2",
75
75
  "@types/unist": "^3.0.3",
76
76
  "@types/yargs": "^17.0.35",
77
+ "@vitest/coverage-v8": "4.1.2",
77
78
  "bumpp": "^11.0.1",
78
79
  "execa": "^9.6.1",
79
80
  "mdat-plugin-cli-help": "^2.0.2",
80
81
  "mdat-plugin-example": "^2.0.0",
81
- "mdat-plugin-tldraw": "^2.0.0",
82
+ "mdat-plugin-tldraw": "^2.0.1",
82
83
  "nanoid": "^5.1.7",
83
84
  "prettier": "^3.8.1",
84
85
  "publint": "^0.3.18",
@@ -109,6 +110,7 @@
109
110
  "bench:baseline": "vitest bench --run --outputJson test/benchmarks/baseline.json",
110
111
  "build": "tsdown",
111
112
  "clean": "git rm -f pnpm-lock.yaml ; git clean -fdX",
113
+ "coverage": "vitest --coverage --run",
112
114
  "fix": "ksc fix",
113
115
  "lint": "ksc lint",
114
116
  "release": "bumpp --commit 'Release: %s' && pnpm run build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') && pnpm publish",
package/readme.md CHANGED
@@ -228,6 +228,7 @@ mdat [command] [files..] [options]
228
228
  | ---------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
229
229
  | `expand` | `[files..]` `[options]` | Expand MDAT placeholder comments. If no files are provided, the closest readme.md is expanded. _(Default command.)_ |
230
230
  | `collapse` | `[files..]` `[options]` | Collapse MDAT placeholder comments. If no files are provided, the closest readme.md is collapsed. |
231
+ | `strip` | `[files..]` `[options]` | Strip MDAT comments while preserving expanded content. If no files are provided, the closest readme.md is stripped. |
231
232
  | `check` | `[files..]` `[options]` | Check if MDAT placeholder comments are up to date. Exits with code 1 if any files have stale or unexpanded content. |
232
233
  | `create` | `[options]` | Create a new Markdown file from a template. |
233
234
 
@@ -282,6 +283,30 @@ mdat collapse [files..] [options]
282
283
  | `--help`<br>`-h` | Show help | `boolean` | |
283
284
  | `--version`<br>`-v` | Show version number | `boolean` | |
284
285
 
286
+ #### Subcommand: `mdat strip`
287
+
288
+ Strip MDAT comments while preserving expanded content. If no files are provided, the closest readme.md is stripped.
289
+
290
+ Usage:
291
+
292
+ ```txt
293
+ mdat strip [files..] [options]
294
+ ```
295
+
296
+ | Positional Argument | Description | Type |
297
+ | ------------------- | ----------------------------------------------------------------------------------------------------- | -------- |
298
+ | `files` | Markdown file(s) with MDAT placeholder comments. If not provided, the closest readme.md file is used. | `string` |
299
+
300
+ | Option | Description | Type | Default |
301
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------- |
302
+ | `--verbose` | Enable verbose logging. All verbose logs are prefixed with their log level and are printed to stderr for ease of redirection. | `boolean` | |
303
+ | `--output`<br>`-o` | Output file directory. | `string` | Same directory as input file. |
304
+ | `--name`<br>`-n` | Output file name. | `string` | Same name as input file. Overwrites the input file. |
305
+ | `--print` | Print the expanded Markdown to stdout instead of saving to a file. Ignores `--output` and `--name` options. | `boolean` | |
306
+ | `--format`<br>`-f` | Format the output with Prettier. Discovers Prettier config from the file path. Requires `prettier` as a peer dependency. | `boolean` | |
307
+ | `--help`<br>`-h` | Show help | `boolean` | |
308
+ | `--version`<br>`-v` | Show version number | `boolean` | |
309
+
285
310
  #### Subcommand: `mdat check`
286
311
 
287
312
  Check if MDAT placeholder comments are up to date. Exits with code 1 if any files have stale or unexpanded content.
@@ -366,6 +391,12 @@ mdat check
366
391
  mdat collapse
367
392
  ```
368
393
 
394
+ ##### Strip MDAT comments from expanded content
395
+
396
+ ```sh
397
+ mdat strip
398
+ ```
399
+
369
400
  ##### Expand and format with Prettier
370
401
 
371
402
  ```sh
@@ -382,19 +413,9 @@ mdat create
382
413
 
383
414
  `mdat` exports functions for expanding, collapsing, checking, and creating Markdown files programmatically.
384
415
 
385
- #### `expand`
416
+ #### `expand` / `expandString`
386
417
 
387
- ```ts
388
- function expand(
389
- files?: string | string[],
390
- name?: string,
391
- output?: string,
392
- config?: ConfigToLoad,
393
- options?: { format?: boolean },
394
- ): Promise<VFile[]>
395
- ```
396
-
397
- Expands MDAT comments in one or more files. If no files are provided, auto-finds the closest readme. Writing is the caller's responsibility:
418
+ Expands MDAT comments in one or more files. If no files are provided, auto-finds the closest readme. Writing is the caller's responsibility. Call `.toString()` on the returned [VFile](https://github.com/vfile) to get the result.
398
419
 
399
420
  ```ts
400
421
  import { expand } from 'mdat'
@@ -404,8 +425,6 @@ const [file] = await expand('readme.md')
404
425
  await write(file)
405
426
  ```
406
427
 
407
- #### `expandString`
408
-
409
428
  ```ts
410
429
  function expandString(
411
430
  markdown: string,
@@ -414,23 +433,19 @@ function expandString(
414
433
  ): Promise<VFile>
415
434
  ```
416
435
 
417
- Expands MDAT comments in a Markdown string. Call `.toString()` on the returned [VFile](https://github.com/vfile) to get the result.
418
-
419
436
  #### `collapse` / `collapseString`
420
437
 
421
438
  Removes expanded content, leaving only the opening comment placeholders. Same signatures as `expand` / `expandString`.
422
439
 
423
- #### `check`
440
+ #### `strip` / `stripString`
424
441
 
425
- ```ts
426
- function check(
427
- files?: string | string[],
428
- config?: ConfigToLoad,
429
- options?: { format?: boolean },
430
- ): Promise<{ inSync: boolean; results: VFile[] }>
431
- ```
442
+ Strips all MDAT comment tags (both opening and closing) while preserving expanded content between them. Same signatures as `expand` / `expandString` (without the `config` parameter, since rules are not needed).
443
+
444
+ This is useful for producing a "clean" Markdown file that no longer depends on MDAT for future updates.
432
445
 
433
- Dry-run expand and compare with the file on disk. Returns `inSync: false` if the file would change.
446
+ #### `check` / `checkString`
447
+
448
+ Dry-run expand and compare with the file on disk. Returns `inSync: false` if the file would change. When stale, per-tag diagnostic messages are added to the result VFile identifying which specific tags are stale or unexpanded. Use `reporterMdat` from `remark-mdat` to display these messages.
434
449
 
435
450
  #### `create` / `createInteractive`
436
451
 
@@ -577,16 +592,16 @@ See the [Examples section](https://github.com/kitschpatrol/remark-mdat#examples)
577
592
 
578
593
  Embeds a file's size, with optional Brotli or Gzip compressed size.
579
594
 
580
- - ##### `<!-- size-table({ files: [".gitignore", "readme.md"] }) -->`
595
+ - ##### `<!-- size-table({ files: [".gitignore", "license.txt"] }) -->`
581
596
 
582
597
  A table of files and their compressed sizes:
583
598
 
584
- <!-- size-table({ files: [".gitignore", "readme.md"] }) -->
599
+ <!-- size-table({ files: [".gitignore", "license.txt"] }) -->
585
600
 
586
- | File | Original | Gzip | Brotli |
587
- | ---------- | -------- | ------ | ------ |
588
- | .gitignore | 305 B | 245 B | 216 B |
589
- | readme.md | 34.4 kB | 8.3 kB | 6.9 kB |
601
+ | File | Original | Gzip | Brotli |
602
+ | ----------- | -------- | ----- | ------ |
603
+ | .gitignore | 305 B | 245 B | 216 B |
604
+ | license.txt | 1 kB | 659 B | 468 B |
590
605
 
591
606
  <!-- /size-table -->
592
607
 
@@ -779,30 +794,6 @@ There's quite a bit of prior art and similar explorations of this problem space:
779
794
 
780
795
  - VitePress' [Markdown file inclusion](https://vitepress.dev/guide/markdown#markdown-file-inclusion)
781
796
 
782
- There's quite a bit of prior art and similar explorations of this problem space:
783
-
784
- - Benjamin Lupton's [projectz](https://github.com/bevry/projectz)\
785
- Goes way back.
786
-
787
- - David Wells' [Markdown Magic](https://github.com/DavidWells/markdown-magic)\
788
- I somehow missed the existence of this one until after building out MDAT. It's very similar conceptually, and has a nice ecosystem of plugins.
789
-
790
- - Titus Wormer's [mdast-zone](https://github.com/syntax-tree/mdast-zone)\
791
- Allows comments to be used as ranges or markers in Markdown files. Similar tree parsing and walking strategy to MDAT. Mdast-zone uses different syntax for arguments, and requires both opening and closing tags to be present for expansion to occur.
792
-
793
- - Jason Dent's [inject-markdown](https://github.com/streetsidesoftware/inject-markdown)
794
-
795
- - lillallol's [md-in-place](https://www.npmjs.com/package/md-in-place)
796
-
797
- - [AutoMD](https://automd.unjs.io/)\
798
- Extremely similar functionality to mdat. The project was initiated around the same time as MDAT, but I didn't find the project until a few years later. Ships in the night.
799
-
800
- - Franck Abgrall's [readme-md-generator](https://github.com/kefranabg/readme-md-generator)
801
-
802
- - Anders Pitman's [tuplates](https://github.com/anderspitman/tuplates-py)
803
-
804
- - VitePress' [Markdown file inclusion](https://vitepress.dev/guide/markdown#markdown-file-inclusion)
805
-
806
797
  ### Implementation notes
807
798
 
808
799
  This project was split from a monorepo containing both `mdat` and `remark-mdat` into separate repos in July 2024.