mdat 2.0.0 → 2.1.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/lib/index.js CHANGED
@@ -4,10 +4,10 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import picocolors from "picocolors";
6
6
  import plur from "plur";
7
- import { getSoleRule, mdatClean, mdatExpand, mdatSplit, rulesSchema, setLogger as setLogger$1 } from "remark-mdat";
8
- import { defineTemplate, getMetadata, helpers, setLogger as setLogger$2, templates } from "metascope";
7
+ import { getSoleRule, mdatCollapse, mdatExpand, mdatSplit, mdatStrip, rulesSchema, setLogger as setLogger$1 } from "remark-mdat";
9
8
  import { deepmerge } from "deepmerge-ts";
10
9
  import { createLogger, getChildLogger, injectionHelper } from "lognow";
10
+ import { defineTemplate, getMetadata, helpers, setLogger as setLogger$2, templates } from "metascope";
11
11
  import { z } from "zod";
12
12
  import { globby } from "globby";
13
13
  import { isFile, isFileSync } from "path-type";
@@ -22,24 +22,111 @@ import { VFile } from "vfile";
22
22
  import { Configuration } from "unified-engine";
23
23
  import untildify from "untildify";
24
24
  import { confirm, group, intro, note, outro, select } from "@clack/prompts";
25
+ //#region src/lib/deep-merge-defined.ts
26
+ function stripUndefinedDeep(object) {
27
+ if (Array.isArray(object)) return object.map((v) => v && typeof v === "object" ? stripUndefinedDeep(v) : v).filter((v) => v !== void 0);
28
+ return Object.entries(object).map(([k, v]) => [k, v && typeof v === "object" ? stripUndefinedDeep(v) : v]).reduce((acc, [k, v]) => v === void 0 ? acc : {
29
+ ...acc,
30
+ [k]: v
31
+ }, {});
32
+ }
33
+ function deepMergeDefined(...objects) {
34
+ return deepmerge(...objects.map((v, i) => i === 0 ? v : stripUndefinedDeep(v)));
35
+ }
36
+ //#endregion
37
+ //#region src/lib/log.ts
38
+ /**
39
+ * The default logger instance for the library.
40
+ */
41
+ let log = createLogger({
42
+ logToConsole: { showTime: false },
43
+ name: "mdat"
44
+ });
45
+ setLogger$1(getChildLogger(log, "remark-mdat"));
46
+ setLogger$2(getChildLogger(log, "metascope"));
47
+ /**
48
+ * Set the logger instance for the module.
49
+ * Export this for library consumers to inject their own logger.
50
+ * @param logger - Accepts either a LogLayer instance or a Console- or Stream-like log target
51
+ */
52
+ function setLogger(logger) {
53
+ log = injectionHelper(logger);
54
+ setLogger$1(getChildLogger(log, "remark-mdat"));
55
+ setLogger$2(getChildLogger(log, "metascope"));
56
+ }
57
+ //#endregion
58
+ //#region src/lib/mdat-json-loader.ts
59
+ /**
60
+ * Lets arbitrary JSON objects (like from package.json) become reasonably good mdat rule sets
61
+ * HOWEVER cosmiconfig treats package.json as a special case and will always load only specific keys from it
62
+ * So we have to intercept and load them manually in config.ts
63
+ */
64
+ function mdatJsonLoader(filePath, content) {
65
+ const defaultJsonLoader = defaultLoaders[".json"];
66
+ return flattenJson(defaultJsonLoader(filePath, content));
67
+ }
68
+ function flattenJson(jsonObject, parentKey = "", result = {}) {
69
+ for (const [key, value] of Object.entries(jsonObject)) {
70
+ const fullPath = parentKey ? `${parentKey}.${key}` : key;
71
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) flattenJson(value, fullPath, result);
72
+ else if (value === null) result[fullPath] = "null";
73
+ else result[fullPath] = value.toString();
74
+ }
75
+ return result;
76
+ }
77
+ //#endregion
25
78
  //#region src/lib/context.ts
26
79
  let metascopeMetadata;
27
80
  /**
28
81
  * Get a bunch of platform-agnostic local metadata via metascope, exposed
29
- * primarily for plugin developers.
30
- * Result is memoized the result.
82
+ * primarily for plugin developers. Result is memoized the result.
83
+ *
31
84
  * @throws {Error} If no package.json is found
32
85
  */
33
86
  async function getContextMetadata() {
34
87
  if (metascopeMetadata !== void 0) return metascopeMetadata;
35
88
  metascopeMetadata = await getMetadata({
36
89
  absolute: false,
37
- offline: true
90
+ offline: true,
91
+ sources: [
92
+ "arduinoLibraryProperties",
93
+ "cinderCinderblockXml",
94
+ "codemetaJson",
95
+ "gitConfig",
96
+ "githubActions",
97
+ "goGoMod",
98
+ "goGoreleaserYaml",
99
+ "javaPomXml",
100
+ "licenseFile",
101
+ "metadataFile",
102
+ "metascope",
103
+ "nodePackageJson",
104
+ "obsidianPluginManifestJson",
105
+ "openframeworksAddonConfigMk",
106
+ "openframeworksInstallXml",
107
+ "processingLibraryProperties",
108
+ "processingSketchProperties",
109
+ "publiccodeYaml",
110
+ "pythonPkgInfo",
111
+ "pythonPyprojectToml",
112
+ "pythonSetupCfg",
113
+ "pythonSetupPy",
114
+ "readmeFile",
115
+ "rubyGemspec",
116
+ "rustCargoToml",
117
+ "xcodeInfoPlist",
118
+ "xcodeProjectPbxproj"
119
+ ]
38
120
  });
39
121
  return metascopeMetadata;
40
122
  }
123
+ const GIT_PREFIX_REGEX = /^git\+/;
124
+ const GIT_SUFFIX_REGEX = /\.git$/;
125
+ const TRAILING_SLASH_REGEX = /\/$/;
41
126
  /**
42
127
  * Reset
128
+ *
129
+ * @public
43
130
  */
44
131
  function resetContextMetadata() {
45
132
  metascopeMetadata = void 0;
@@ -50,7 +137,7 @@ const readmeMetadataTemplate = defineTemplate((context) => {
50
137
  const nodePackage = helpers.firstOf(nodePackageJson)?.data;
51
138
  const licenseFileData = helpers.firstOf(licenseFile);
52
139
  const ciActionFilePath = helpers.ensureArray(githubActions).find((entry) => entry.data.name.toLowerCase() === "ci")?.source;
53
- const repositoryOwner = codemeta.codeRepository ? new URL(codemeta.codeRepository).pathname.split("/")[1] : void 0;
140
+ const repositoryUrl = codemeta.codeRepository?.replace(GIT_PREFIX_REGEX, "").replace(GIT_SUFFIX_REGEX, "").replace(TRAILING_SLASH_REGEX, "");
54
141
  return {
55
142
  author: helpers.firstOf(helpers.mixedStringsToArray(helpers.toBasicNames(codemeta.author))),
56
143
  ciActionFileName: ciActionFilePath ? path.basename(ciActionFilePath) : void 0,
@@ -61,12 +148,14 @@ const readmeMetadataTemplate = defineTemplate((context) => {
61
148
  licenseFilePath: licenseFileData?.source,
62
149
  name: codemeta.name,
63
150
  projectDirectory: metascope?.data.options.path === void 0 ? void 0 : `file://${metascope.data.options.path}`,
64
- repositoryOwner
151
+ repositoryUrl
65
152
  };
66
153
  });
67
154
  let readmeMetadata;
68
155
  /**
69
156
  * Nice data for readme rules
157
+ *
158
+ * @public
70
159
  */
71
160
  async function getReadmeMetadata() {
72
161
  if (readmeMetadata !== void 0) return readmeMetadata;
@@ -75,62 +164,19 @@ async function getReadmeMetadata() {
75
164
  }
76
165
  /**
77
166
  * Reset
167
+ *
168
+ * @public
78
169
  */
79
170
  function resetReadmeMetadata() {
80
171
  readmeMetadata = void 0;
81
172
  }
82
- //#endregion
83
- //#region src/lib/deep-merge-defined.ts
84
- function stripUndefinedDeep(object) {
85
- if (Array.isArray(object)) return object.map((v) => v && typeof v === "object" ? stripUndefinedDeep(v) : v).filter((v) => v !== void 0);
86
- return Object.entries(object).map(([k, v]) => [k, v && typeof v === "object" ? stripUndefinedDeep(v) : v]).reduce((acc, [k, v]) => v === void 0 ? acc : {
87
- ...acc,
88
- [k]: v
89
- }, {});
90
- }
91
- function deepMergeDefined(...objects) {
92
- return deepmerge(...objects.map((v, i) => i === 0 ? v : stripUndefinedDeep(v)));
93
- }
94
- //#endregion
95
- //#region src/lib/log.ts
96
- /**
97
- * The default logger instance for the library.
98
- */
99
- let log = createLogger({
100
- logToConsole: { showTime: false },
101
- name: "mdat"
102
- });
103
- setLogger$1(getChildLogger(log, "remark-mdat"));
104
- setLogger$2(getChildLogger(log, "metascope"));
105
- /**
106
- * Set the logger instance for the module.
107
- * Export this for library consumers to inject their own logger.
108
- * @param logger - Accepts either a LogLayer instance or a Console- or Stream-like log target
109
- */
110
- function setLogger(logger) {
111
- log = injectionHelper(logger);
112
- setLogger$1(getChildLogger(log, "remark-mdat"));
113
- setLogger$2(getChildLogger(log, "metascope"));
114
- }
115
- //#endregion
116
- //#region src/lib/mdat-json-loader.ts
117
173
  /**
118
- * Lets arbitrary JSON objects (like from package.json) become reasonably good mdat rule sets
119
- * HOWEVER cosmiconfig treats package.json as a special case and will always load only specific keys from it
120
- * So we have to intercept and load them manually in config.ts
174
+ * Reset all cached metadata. Call between tests or when the underlying project
175
+ * files may have changed on disk.
121
176
  */
122
- function mdatJsonLoader(filePath, content) {
123
- const defaultJsonLoader = defaultLoaders[".json"];
124
- return flattenJson(defaultJsonLoader(filePath, content));
125
- }
126
- function flattenJson(jsonObject, parentKey = "", result = {}) {
127
- for (const [key, value] of Object.entries(jsonObject)) {
128
- const fullPath = parentKey ? `${parentKey}.${key}` : key;
129
- if (typeof value === "object" && value !== null && !Array.isArray(value)) flattenJson(value, fullPath, result);
130
- else if (value === null) result[fullPath] = "null";
131
- else result[fullPath] = value.toString();
132
- }
133
- return result;
177
+ function resetMetadataCaches() {
178
+ resetContextMetadata();
179
+ resetReadmeMetadata();
134
180
  }
135
181
  //#endregion
136
182
  //#region src/lib/readme/rules/badges.ts
@@ -143,13 +189,13 @@ var badges_default = { badges: { async content(options) {
143
189
  npm: z.array(z.string()).optional()
144
190
  }).optional().parse(options);
145
191
  const metadata = await getReadmeMetadata();
146
- const { ciActionFileName, license, name, repositoryOwner } = metadata;
192
+ const { ciActionFileName, license, name, repositoryUrl } = metadata;
147
193
  const badges = [];
148
194
  if (validOptions?.npm === void 0) {
149
195
  if (metadata.isPublicNpmPackage) badges.push(`[![NPM Package ${name}](https://img.shields.io/npm/v/${name}.svg)](https://npmjs.com/package/${name})`);
150
196
  } else for (const name of validOptions.npm) badges.push(`[![NPM Package ${name}](https://img.shields.io/npm/v/${name}.svg)](https://npmjs.com/package/${name})`);
151
197
  if (license !== void 0) badges.push(`[![License: ${license}](https://img.shields.io/badge/License-${license.replaceAll("-", "--")}-yellow.svg)](https://opensource.org/licenses/${license})`);
152
- if (ciActionFileName !== void 0 && repositoryOwner !== void 0) badges.push(`[![CI](https://github.com/${repositoryOwner}/${name}/actions/workflows/${ciActionFileName}/badge.svg)](https://github.com/${repositoryOwner}/${name}/actions/workflows/${ciActionFileName})`);
198
+ if (ciActionFileName !== void 0 && repositoryUrl !== void 0) badges.push(`[![CI](${repositoryUrl}/actions/workflows/${ciActionFileName}/badge.svg)](${repositoryUrl}/actions/workflows/${ciActionFileName})`);
153
199
  if (validOptions?.custom !== void 0) for (const [name, { image, link }] of Object.entries(validOptions.custom)) badges.push(`[![${name}](${image})](${link})`);
154
200
  return badges.join("\n");
155
201
  } } };
@@ -216,23 +262,20 @@ function isUrl(text, lenient = true) {
216
262
  if (typeof text !== "string") throw new TypeError("Expected a string");
217
263
  text = text.trim();
218
264
  if (text.includes(" ")) return false;
219
- try {
220
- new URL(text);
221
- return true;
222
- } catch {
223
- if (lenient) return isUrl(`https://${text}`, false);
224
- return false;
225
- }
265
+ if (URL.canParse(text)) return true;
266
+ if (lenient) return isUrl(`https://${text}`, false);
267
+ return false;
226
268
  }
227
269
  //#endregion
228
270
  //#region src/lib/readme/rules/code.ts
271
+ const LEADING_DOT_REGEX = /^\./;
229
272
  var code_default = { code: { async content(options) {
230
273
  const validOptions = z.object({
231
274
  file: z.string(),
232
275
  language: z.string().optional(),
233
276
  trim: z.boolean().default(true)
234
277
  }).parse(options);
235
- const lang = (path.extname(validOptions.file) ?? "").replace(/^\./, "");
278
+ const lang = (path.extname(validOptions.file) ?? "").replace(LEADING_DOT_REGEX, "");
236
279
  const exampleCode = await fs.readFile(path.join(process.cwd(), validOptions.file), "utf8");
237
280
  return `\`\`\`${lang}\n${validOptions.trim ? exampleCode.trim() : exampleCode}\n\`\`\``;
238
281
  } } };
@@ -287,8 +330,9 @@ var title_default = { title: {
287
330
  },
288
331
  order: 2
289
332
  } };
333
+ const SPLIT_FOR_TITLE_CASE_REGEX = /[ _-]+/;
290
334
  function makeTitleCase(text) {
291
- return text.split(/[ _-]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
335
+ return text.split(SPLIT_FOR_TITLE_CASE_REGEX).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
292
336
  }
293
337
  //#endregion
294
338
  //#region src/lib/readme/rules/header.ts
@@ -467,6 +511,19 @@ var rules_default = {
467
511
  };
468
512
  //#endregion
469
513
  //#region src/lib/config.ts
514
+ let _configExplorer;
515
+ let _additionalConfigExplorer;
516
+ function getConfigExplorer() {
517
+ _configExplorer ??= cosmiconfig("mdat", { loaders: { ".ts": TypeScriptLoader() } });
518
+ return _configExplorer;
519
+ }
520
+ function getAdditionalConfigExplorer() {
521
+ _additionalConfigExplorer ??= cosmiconfig("mdat", { loaders: {
522
+ ".json": mdatJsonLoader,
523
+ ".ts": TypeScriptLoader()
524
+ } });
525
+ return _additionalConfigExplorer;
526
+ }
470
527
  /**
471
528
  * Load and validate mdat configuration.
472
529
  * Uses cosmiconfig to search in the usual places.
@@ -474,11 +531,9 @@ var rules_default = {
474
531
  */
475
532
  async function loadConfig(options) {
476
533
  const { additionalConfig, defaults = rules_default, searchFrom } = options ?? {};
477
- resetReadmeMetadata();
478
- resetContextMetadata();
479
534
  let finalConfig = { mdat: `Powered by the Markdown Autophagic Template system: [mdat](https://github.com/kitschpatrol/mdat).` };
480
535
  if (defaults) finalConfig = deepMergeDefined(finalConfig, defaults);
481
- const results = await cosmiconfig("mdat", { loaders: { ".ts": TypeScriptLoader() } }).search(searchFrom);
536
+ const results = await getConfigExplorer().search(searchFrom);
482
537
  if (results) {
483
538
  const { config, filepath } = results;
484
539
  let possibleRules = config;
@@ -493,10 +548,6 @@ async function loadConfig(options) {
493
548
  }
494
549
  if (additionalConfig !== void 0) {
495
550
  const additionalConfigArray = Array.isArray(additionalConfig) ? additionalConfig : [additionalConfig];
496
- const configExplorer2 = cosmiconfig("mdat", { loaders: {
497
- ".json": mdatJsonLoader,
498
- ".ts": TypeScriptLoader()
499
- } });
500
551
  for (const configOrPath of additionalConfigArray) {
501
552
  let loaded;
502
553
  if (typeof configOrPath === "string") {
@@ -505,7 +556,7 @@ async function loadConfig(options) {
505
556
  config: mdatJsonLoader(configOrPath, await fs.readFile(configOrPath, "utf8")),
506
557
  filepath: configOrPath
507
558
  };
508
- else results = await configExplorer2.load(configOrPath);
559
+ else results = await getAdditionalConfigExplorer().load(configOrPath);
509
560
  if (results === null || results === void 0) continue;
510
561
  const { config: loadedConfig, filepath } = results;
511
562
  log.debug(`Loaded additional config from "${filepath}"`);
@@ -541,20 +592,26 @@ function defineConfig(config) {
541
592
  }
542
593
  //#endregion
543
594
  //#region src/lib/format.ts
595
+ let cachedPrettier;
596
+ const configCache = /* @__PURE__ */ new Map();
544
597
  /**
545
598
  * Format a markdown string with Prettier, using config discovered from the file path.
546
599
  * Requires `prettier` to be installed as a peer dependency.
547
600
  */
548
601
  async function formatWithPrettier(content, filePath) {
549
- let prettier;
550
- try {
551
- prettier = await import("prettier");
602
+ if (cachedPrettier === void 0) try {
603
+ cachedPrettier = await import("prettier");
552
604
  } catch {
553
605
  throw new Error("The --format flag requires `prettier` to be installed. Run: pnpm add -D prettier");
554
606
  }
555
- const config = await prettier.resolveConfig(filePath ?? process.cwd());
556
- if (config) log.debug(`Using Prettier config from "${config.filepath}" for "${filePath ?? process.cwd()}"`);
557
- return prettier.format(content, {
607
+ const configKey = filePath ? path.dirname(filePath) : process.cwd();
608
+ let config = configCache.get(configKey);
609
+ if (config === void 0 && !configCache.has(configKey)) {
610
+ config = await cachedPrettier.resolveConfig(filePath ?? process.cwd());
611
+ configCache.set(configKey, config);
612
+ if (config) log.debug(`Using Prettier config from "${config.filepath}" for "${filePath ?? process.cwd()}"`);
613
+ }
614
+ return cachedPrettier.format(content, {
558
615
  ...config,
559
616
  filepath: filePath,
560
617
  parser: "markdown"
@@ -595,12 +652,13 @@ function ensureArray(value) {
595
652
  if (value === void 0 || value === null) return [];
596
653
  return Array.isArray(value) ? value : [value];
597
654
  }
655
+ const README_SEARCH_REGEX = /^readme(?:\.\w+)?$/i;
598
656
  /**
599
657
  * Finds a readme file in the current working directory (case-insensitive).
600
658
  */
601
659
  async function findReadme() {
602
660
  log.debug("Searching for readme in current directory...");
603
- const readme = (await fs.readdir(process.cwd())).find((entry) => /^readme(?:\.\w+)?$/i.test(entry));
661
+ const readme = (await fs.readdir(process.cwd())).find((entry) => README_SEARCH_REGEX.test(entry));
604
662
  if (readme !== void 0) {
605
663
  const absolutePath = path.resolve(readme);
606
664
  log.debug(`Found readme at "${absolutePath}"`);
@@ -616,7 +674,9 @@ async function findReadmeThrows() {
616
674
  if (readme === void 0) throw new Error("No readme found");
617
675
  return readme;
618
676
  }
677
+ let cachedAmbientRemarkConfig;
619
678
  async function loadAmbientRemarkConfig() {
679
+ if (cachedAmbientRemarkConfig !== void 0) return cachedAmbientRemarkConfig;
620
680
  const ambientConfig = new Configuration({
621
681
  cwd: process.cwd(),
622
682
  detectConfig: true,
@@ -637,34 +697,50 @@ async function loadAmbientRemarkConfig() {
637
697
  const { filePath } = configResult;
638
698
  if (filePath === void 0) log.debug("No ambient Remark configuration file found");
639
699
  else log.debug(`Found and loaded ambient Remark configuration from "${filePath}"`);
640
- return configResult;
700
+ cachedAmbientRemarkConfig = stripLintPlugins(configResult);
701
+ return cachedAmbientRemarkConfig;
641
702
  }
642
703
  log.debug("No ambient Remark configuration found");
643
- return {
704
+ cachedAmbientRemarkConfig = {
644
705
  filePath: void 0,
645
706
  plugins: [],
646
707
  settings: {}
647
708
  };
709
+ return cachedAmbientRemarkConfig;
710
+ }
711
+ /**
712
+ * Strip remark-lint plugins from an ambient config. Lint plugins only produce
713
+ * VFile warnings and never modify the AST or output — running them during
714
+ * expansion is pure overhead.
715
+ */
716
+ function stripLintPlugins(config) {
717
+ return {
718
+ ...config,
719
+ plugins: config.plugins.filter((entry) => {
720
+ const plugin = Array.isArray(entry) ? entry[0] : entry;
721
+ if (typeof plugin !== "function") return true;
722
+ const { name } = plugin;
723
+ return name !== "remarkLint" && !name.startsWith("remark-lint:") && name !== "remarkValidateLinks";
724
+ })
725
+ };
648
726
  }
649
727
  //#endregion
650
728
  //#region src/lib/processors.ts
651
729
  async function processFiles(files, loader, processorGetter, name, output, config) {
652
- const resolvedConfig = await loader({ additionalConfig: config });
653
- const localRemarkConfiguration = await loadAmbientRemarkConfig();
730
+ const [resolvedConfig, localRemarkConfiguration] = await Promise.all([loader({ additionalConfig: config }), loadAmbientRemarkConfig()]);
654
731
  const inputOutputPaths = await getInputOutputPaths(ensureArray(files), output, name, "md");
655
- const results = [];
656
732
  const resolvedProcessor = processorGetter(resolvedConfig, localRemarkConfiguration);
657
- for (const { input, name, output } of inputOutputPaths) {
733
+ return await Promise.all(inputOutputPaths.map(async ({ input, name, output }) => {
658
734
  const inputFile = await read(input);
659
735
  const result = await resolvedProcessor.process(inputFile);
660
736
  result.dirname = output;
661
737
  result.basename = name;
662
- results.push(result);
663
- }
664
- return results;
738
+ return result;
739
+ }));
665
740
  }
666
741
  async function processString(markdown, loader, processorGetter, config) {
667
- return processorGetter(await loader({ additionalConfig: config }), await loadAmbientRemarkConfig()).process(new VFile(markdown));
742
+ const [resolvedConfig, localRemarkConfiguration] = await Promise.all([loader({ additionalConfig: config }), loadAmbientRemarkConfig()]);
743
+ return processorGetter(resolvedConfig, localRemarkConfiguration).process(new VFile(markdown));
668
744
  }
669
745
  function getExpandProcessor(config, ambientRemarkConfig) {
670
746
  return remark().use({ settings: {
@@ -672,17 +748,26 @@ function getExpandProcessor(config, ambientRemarkConfig) {
672
748
  emphasis: "_"
673
749
  } }).use(remarkGfm).use(ambientRemarkConfig).use(() => async function(tree, file) {
674
750
  mdatSplit(tree, file);
675
- mdatClean(tree, file);
751
+ mdatCollapse(tree, file);
676
752
  await mdatExpand(tree, file, config);
677
753
  });
678
754
  }
679
- function getCleanProcessor(_config, ambientRemarkConfig) {
755
+ function getCollapseProcessor(_config, ambientRemarkConfig) {
680
756
  return remark().use({ settings: {
681
757
  bullet: "-",
682
758
  emphasis: "_"
683
759
  } }).use(remarkGfm).use(ambientRemarkConfig).use(() => function(tree, file) {
684
760
  mdatSplit(tree, file);
685
- mdatClean(tree, file);
761
+ mdatCollapse(tree, file);
762
+ });
763
+ }
764
+ function getStripProcessor(_config, ambientRemarkConfig) {
765
+ return remark().use({ settings: {
766
+ bullet: "-",
767
+ emphasis: "_"
768
+ } }).use(remarkGfm).use(ambientRemarkConfig).use(() => function(tree, file) {
769
+ mdatSplit(tree, file);
770
+ mdatStrip(tree, file);
686
771
  });
687
772
  }
688
773
  //#endregion
@@ -812,10 +897,11 @@ function getTemplateOptions() {
812
897
  //#endregion
813
898
  //#region src/lib/api.ts
814
899
  /**
815
- * Expand MDAT comments in one or more Markdown files.
816
- * If no files are provided, auto-finds the closest readme.md.
817
- * Writing is the responsibility of the caller (e.g. via `await write(result)`)
818
- * @returns an array of VFiles
900
+ * Expand MDAT comments in one or more Markdown files. If no files are provided,
901
+ * auto-finds the closest readme.md. Writing is the responsibility of the caller
902
+ * (e.g. via `await write(result)`)
903
+ *
904
+ * @returns An array of VFiles
819
905
  */
820
906
  async function expand(files, name, output, config, options) {
821
907
  files ??= await findReadmeThrows();
@@ -832,14 +918,15 @@ async function expandString(markdown, config, options) {
832
918
  return result;
833
919
  }
834
920
  /**
835
- * Collapse MDAT comments in one or more Markdown files.
836
- * If no files are provided, auto-finds the closest readme.md.
837
- * Writing is the responsibility of the caller (e.g. via `await write(result)`)
838
- * @returns an array of VFiles
921
+ * Collapse MDAT comments in one or more Markdown files. If no files are
922
+ * provided, auto-finds the closest readme.md. Writing is the responsibility of
923
+ * the caller (e.g. via `await write(result)`)
924
+ *
925
+ * @returns An array of VFiles
839
926
  */
840
927
  async function collapse(files, name, output, config, options) {
841
928
  files ??= await findReadmeThrows();
842
- const results = await processFiles(files, loadConfig, getCleanProcessor, name, output, config);
929
+ const results = await processFiles(files, loadConfig, getCollapseProcessor, name, output, config);
843
930
  if (options?.format) await formatResults(results);
844
931
  return results;
845
932
  }
@@ -847,24 +934,46 @@ async function collapse(files, name, output, config, options) {
847
934
  * Collapse MDAT comments in a Markdown string.
848
935
  */
849
936
  async function collapseString(markdown, config, options) {
850
- const result = await processString(markdown, loadConfig, getCleanProcessor, config);
937
+ const result = await processString(markdown, loadConfig, getCollapseProcessor, config);
938
+ if (options?.format) await formatResults([result]);
939
+ return result;
940
+ }
941
+ /**
942
+ * Strips MDAT comments in one or more Markdown files without touching other
943
+ * content. Does _not_ automatically expand Mdat content before stripping the
944
+ * tags. If no files are provided, auto-finds the closest readme.md. Writing is
945
+ * the responsibility of the caller (e.g. via `await write(result)`)
946
+ *
947
+ * @returns An array of VFiles
948
+ */
949
+ async function strip(files, name, output, config, options) {
950
+ files ??= await findReadmeThrows();
951
+ const results = await processFiles(files, loadConfig, getStripProcessor, name, output, config);
952
+ if (options?.format) await formatResults(results);
953
+ return results;
954
+ }
955
+ /**
956
+ * Strip MDAT comments from a Markdown string.
957
+ */
958
+ async function stripString(markdown, config, options) {
959
+ const result = await processString(markdown, loadConfig, getStripProcessor, config);
851
960
  if (options?.format) await formatResults([result]);
852
961
  return result;
853
962
  }
854
963
  /**
855
- * Dry-run expand and compare with file on disk.
856
- * If no files are provided, auto-finds the closest readme.md.
857
- * @returns per-file sync status and expanded VFiles
964
+ * Dry-run expand and compare with file on disk. If no files are provided,
965
+ * auto-finds the closest readme.md.
966
+ *
967
+ * @returns Per-file sync status and expanded VFiles
858
968
  */
859
969
  async function check(files, config, options) {
860
970
  files ??= await findReadmeThrows();
861
971
  const { read } = await import("to-vfile");
862
972
  const resolvedFiles = Array.isArray(files) ? files : [files];
863
- const originals = await Promise.all(resolvedFiles.map(async (f) => read(f)));
864
- const results = await processFiles(files, loadConfig, getExpandProcessor, void 0, void 0, config);
973
+ const [originals, results] = await Promise.all([Promise.all(resolvedFiles.map(async (f) => read(f))), processFiles(files, loadConfig, getExpandProcessor, void 0, void 0, config)]);
865
974
  if (options?.format) await formatResults(results);
866
975
  return results.map((result, i) => ({
867
- inSync: originals[i].toString() === result.toString(),
976
+ inSync: originals[i].toString().replaceAll("\r\n", "\n") === result.toString(),
868
977
  result
869
978
  }));
870
979
  }
@@ -872,4 +981,4 @@ async function formatResults(results) {
872
981
  for (const file of results) file.value = await formatWithPrettier(file.toString(), file.path || void 0);
873
982
  }
874
983
  //#endregion
875
- export { check, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, setLogger };
984
+ export { check, 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.0",
3
+ "version": "2.1.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",
@@ -48,9 +48,9 @@
48
48
  "cosmiconfig-typescript-loader": "^6.2.0",
49
49
  "deepmerge-ts": "^7.1.5",
50
50
  "globby": "^16.2.0",
51
- "lognow": "^0.5.2",
51
+ "lognow": "^0.6.0",
52
52
  "mdast-util-toc": "^7.1.0",
53
- "metascope": "^0.4.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.0",
61
+ "remark-mdat": "^2.1.0",
62
62
  "to-vfile": "^8.0.0",
63
63
  "type-fest": "^5.5.0",
64
64
  "unified-engine": "^11.2.2",
@@ -69,13 +69,16 @@
69
69
  },
70
70
  "devDependencies": {
71
71
  "@arethetypeswrong/core": "^0.18.2",
72
- "@kitschpatrol/shared-config": "^6.2.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
77
  "bumpp": "^11.0.1",
78
78
  "execa": "^9.6.1",
79
+ "mdat-plugin-cli-help": "^2.0.2",
80
+ "mdat-plugin-example": "^2.0.0",
81
+ "mdat-plugin-tldraw": "^2.0.0",
79
82
  "nanoid": "^5.1.7",
80
83
  "prettier": "^3.8.1",
81
84
  "publint": "^0.3.18",
@@ -102,11 +105,13 @@
102
105
  }
103
106
  },
104
107
  "scripts": {
108
+ "bench": "vitest bench --run --compare test/benchmarks/baseline.json",
109
+ "bench:baseline": "vitest bench --run --outputJson test/benchmarks/baseline.json",
105
110
  "build": "tsdown",
106
111
  "clean": "git rm -f pnpm-lock.yaml ; git clean -fdX",
107
112
  "fix": "ksc fix",
108
113
  "lint": "ksc lint",
109
114
  "release": "bumpp --commit 'Release: %s' && pnpm run build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') && pnpm publish",
110
- "test": "vitest run --no-file-parallelism"
115
+ "test": "vitest run"
111
116
  }
112
117
  }