@takazudo/mdx-formatter 0.1.2 → 0.3.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/cli.js CHANGED
@@ -4,6 +4,7 @@ import { program } from 'commander';
4
4
  import { glob } from 'glob';
5
5
  import chalk from 'chalk';
6
6
  import { formatFile, checkFile } from './index.js';
7
+ import { loadFullConfig } from './load-config.js';
7
8
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
8
9
  program
9
10
  .name('mdx-formatter')
@@ -29,12 +30,11 @@ program.parse();
29
30
  * Main CLI function
30
31
  */
31
32
  async function main(patterns, options) {
32
- const ignorePatterns = options.ignore.split(',').map((p) => p.trim());
33
- // Build format options from CLI flags
34
- const formatOptions = {};
35
- if (options.config) {
36
- formatOptions.config = options.config;
37
- }
33
+ const cliIgnorePatterns = options.ignore.split(',').map((p) => p.trim());
34
+ // Load config once: get both formatter settings and exclude patterns
35
+ const { settings, excludePatterns } = loadFullConfig(options.config ? { config: options.config } : {});
36
+ const ignorePatterns = [...new Set([...cliIgnorePatterns, ...excludePatterns])];
37
+ const formatOptions = { settings };
38
38
  // Find all matching files
39
39
  const files = [];
40
40
  for (const pattern of patterns) {
@@ -53,6 +53,12 @@ export declare class HybridFormatter {
53
53
  extractHtmlFromNode(node: MdxJsxElement): string | null;
54
54
  collectJsxIndentOperations(operations: FormatterOperation[]): void;
55
55
  collectBlockJsxEmptyLineOperations(operations: FormatterOperation[]): void;
56
+ /**
57
+ * Pre-process YAML text to fix values that would cause parsing failures
58
+ * or silent data corruption. Detects unquoted values containing special
59
+ * YAML characters and wraps them in double quotes.
60
+ */
61
+ preprocessYamlForParsing(yamlText: string): string;
56
62
  collectYamlFormatOperations(operations: FormatterOperation[]): void;
57
63
  getLineAtPosition(charPos: number): number;
58
64
  applyOperation(lines: string[], op: FormatterOperation): void;
@@ -893,16 +893,69 @@ export class HybridFormatter {
893
893
  }
894
894
  });
895
895
  }
896
+ /**
897
+ * Pre-process YAML text to fix values that would cause parsing failures
898
+ * or silent data corruption. Detects unquoted values containing special
899
+ * YAML characters and wraps them in double quotes.
900
+ */
901
+ preprocessYamlForParsing(yamlText) {
902
+ const lines = yamlText.split('\n');
903
+ const result = [];
904
+ for (const line of lines) {
905
+ // Match a YAML mapping entry: optional indent, key, colon, space, value
906
+ // Keys must start with a word char, may contain word chars, dots, hyphens
907
+ const match = line.match(/^(\s*)([\w][\w.-]*):\s+(.+)$/);
908
+ if (match) {
909
+ const [, indent, key, value] = match;
910
+ const trimmedValue = value.trim();
911
+ // Skip if already quoted
912
+ if ((trimmedValue.startsWith('"') && trimmedValue.endsWith('"')) ||
913
+ (trimmedValue.startsWith("'") && trimmedValue.endsWith("'"))) {
914
+ result.push(line);
915
+ continue;
916
+ }
917
+ // Skip if the value is a flow sequence [...] or flow mapping {...}
918
+ if ((trimmedValue.startsWith('[') && trimmedValue.endsWith(']')) ||
919
+ (trimmedValue.startsWith('{') && trimmedValue.endsWith('}'))) {
920
+ result.push(line);
921
+ continue;
922
+ }
923
+ // Skip block scalar indicators (>, |, >-, |-, >+, |+)
924
+ if (/^[|>][-+]?$/.test(trimmedValue)) {
925
+ result.push(line);
926
+ continue;
927
+ }
928
+ const needsQuoting = trimmedValue.includes(': ') ||
929
+ trimmedValue.includes(' #') ||
930
+ /^[!&*%@`]/.test(trimmedValue);
931
+ if (needsQuoting) {
932
+ const escaped = trimmedValue.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
933
+ result.push(`${indent}${key}: "${escaped}"`);
934
+ continue;
935
+ }
936
+ }
937
+ result.push(line);
938
+ }
939
+ return result.join('\n');
940
+ }
896
941
  collectYamlFormatOperations(operations) {
897
942
  const yamlSettings = this.settings.formatYamlFrontmatter;
898
943
  visit(this.ast, (node) => {
899
944
  if (node.type === 'yaml' && node.position) {
900
945
  const yamlNode = node;
901
946
  try {
902
- // Parse the YAML content
903
- const parsed = yaml.load(yamlNode.value);
904
- // Format it back with proper formatting
947
+ let yamlToParse = yamlNode.value;
948
+ // Pre-process YAML to fix unsafe values (e.g., unquoted colons)
949
+ if (yamlSettings.fixUnsafeValues !== false) {
950
+ yamlToParse = this.preprocessYamlForParsing(yamlToParse);
951
+ }
952
+ // Parse the YAML content using JSON_SCHEMA to prevent silent
953
+ // data corruption (e.g., dates parsed as Date objects, octals)
954
+ const parsed = yaml.load(yamlToParse, { schema: yaml.JSON_SCHEMA });
955
+ // Format it back with proper formatting using JSON_SCHEMA
956
+ // to preserve string representations (dates, etc.)
905
957
  const formatted = yaml.dump(parsed, {
958
+ schema: yaml.JSON_SCHEMA,
906
959
  indent: yamlSettings.indent || 2,
907
960
  lineWidth: yamlSettings.lineWidth || 100,
908
961
  quotingType: (yamlSettings.quotingType || '"'),
@@ -7,6 +7,19 @@
7
7
  * 3. Programmatic options (passed to format())
8
8
  */
9
9
  import type { FormatterSettings, FormatOptions } from './types.js';
10
+ /**
11
+ * Load config file once and return both formatter settings and exclude patterns.
12
+ * This avoids reading the config file multiple times in the CLI path.
13
+ */
14
+ export declare function loadFullConfig(options?: FormatOptions): {
15
+ settings: FormatterSettings;
16
+ excludePatterns: string[];
17
+ };
18
+ /**
19
+ * Load exclude patterns from config file.
20
+ * These are glob patterns for files the CLI should skip.
21
+ */
22
+ export declare function loadExcludePatterns(configPath?: string): string[];
10
23
  /**
11
24
  * Load and merge all configuration layers
12
25
  */
@@ -53,19 +53,40 @@ function findConfigFile(configPath) {
53
53
  return null;
54
54
  }
55
55
  /**
56
- * Load and merge all configuration layers
56
+ * Load config file once and return both formatter settings and exclude patterns.
57
+ * This avoids reading the config file multiple times in the CLI path.
57
58
  */
58
- export function loadConfig(options = {}) {
59
+ export function loadFullConfig(options = {}) {
59
60
  // Layer 1: Built-in defaults
60
61
  let settings = deepCloneSettings(formatterSettings);
61
62
  // Layer 2: Config file
62
63
  const fileConfig = findConfigFile(options.config);
64
+ // Extract exclude patterns (CLI concern, not formatter settings)
65
+ let excludePatterns = [];
63
66
  if (fileConfig) {
64
- settings = mergeSettings(settings, fileConfig);
67
+ if (Array.isArray(fileConfig.exclude)) {
68
+ excludePatterns = fileConfig.exclude.filter((item) => typeof item === 'string');
69
+ }
70
+ const formatterConfig = { ...fileConfig };
71
+ delete formatterConfig.exclude;
72
+ settings = mergeSettings(settings, formatterConfig);
65
73
  }
66
74
  // Layer 3: Programmatic options
67
75
  if (options.settings) {
68
76
  settings = mergeSettings(settings, options.settings);
69
77
  }
70
- return settings;
78
+ return { settings, excludePatterns };
79
+ }
80
+ /**
81
+ * Load exclude patterns from config file.
82
+ * These are glob patterns for files the CLI should skip.
83
+ */
84
+ export function loadExcludePatterns(configPath) {
85
+ return loadFullConfig({ config: configPath }).excludePatterns;
86
+ }
87
+ /**
88
+ * Load and merge all configuration layers
89
+ */
90
+ export function loadConfig(options = {}) {
91
+ return loadFullConfig(options).settings;
71
92
  }
package/dist/settings.js CHANGED
@@ -60,6 +60,7 @@ export const formatterSettings = {
60
60
  quotingType: '"', // Quote type for strings that need quoting: '"' or "'"
61
61
  forceQuotes: false, // Force quotes on all string values
62
62
  noCompatMode: true, // Use YAML 1.2 spec (not 1.1)
63
+ fixUnsafeValues: true, // Pre-process YAML to quote values containing special characters like colons
63
64
  },
64
65
  // Rule 8: Preserve Docusaurus admonitions
65
66
  preserveAdmonitions: {
package/dist/types.d.ts CHANGED
@@ -111,6 +111,7 @@ export interface FormatYamlFrontmatterSetting {
111
111
  quotingType: string;
112
112
  forceQuotes: boolean;
113
113
  noCompatMode: boolean;
114
+ fixUnsafeValues: boolean;
114
115
  }
115
116
  export interface PreserveAdmonitionsSetting {
116
117
  enabled: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/mdx-formatter",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "AST-based markdown and MDX formatter with Japanese text support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,6 +33,8 @@
33
33
  "lint:fix": "eslint --fix .",
34
34
  "check": "prettier --check . && eslint .",
35
35
  "check:fix": "prettier --write . && eslint --fix .",
36
+ "b4push": "./scripts/run-b4push.sh",
37
+ "doc:start": "pnpm --dir doc start",
36
38
  "prepare": "husky",
37
39
  "prepublishOnly": "tsc && vitest run"
38
40
  },