@takazudo/mdx-formatter 0.2.0 → 0.4.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.
@@ -29,6 +29,10 @@ export declare class HybridFormatter {
29
29
  collectJsxFormatOperations(operations: FormatterOperation[]): void;
30
30
  needsJsxFormatting(node: MdxJsxElement, originalText: string): boolean;
31
31
  formatJsxElement(node: MdxJsxElement, originalText: string): string;
32
+ /**
33
+ * Check if template literal indentation should be preserved based on settings.
34
+ */
35
+ shouldPreserveTemplateLiteral(): boolean;
32
36
  getAttributeString(attr: MdxJsxAttribute, originalText: string): string;
33
37
  extractAttributeExpression(attrName: string, originalText: string): string | null;
34
38
  extractExpressionValue(expr: MdxJsxAttributeValueExpression): string;
@@ -53,6 +57,12 @@ export declare class HybridFormatter {
53
57
  extractHtmlFromNode(node: MdxJsxElement): string | null;
54
58
  collectJsxIndentOperations(operations: FormatterOperation[]): void;
55
59
  collectBlockJsxEmptyLineOperations(operations: FormatterOperation[]): void;
60
+ /**
61
+ * Pre-process YAML text to fix values that would cause parsing failures
62
+ * or silent data corruption. Detects unquoted values containing special
63
+ * YAML characters and wraps them in double quotes.
64
+ */
65
+ preprocessYamlForParsing(yamlText: string): string;
56
66
  collectYamlFormatOperations(operations: FormatterOperation[]): void;
57
67
  getLineAtPosition(charPos: number): number;
58
68
  applyOperation(lines: string[], op: FormatterOperation): void;
@@ -499,15 +499,21 @@ export class HybridFormatter {
499
499
  // Add each attribute on its own line with proper indent
500
500
  for (const attr of attributes) {
501
501
  const attrStr = this.getAttributeString(attr, originalText);
502
- // Handle multi-line expression values (like arrays)
502
+ // Handle multi-line expression values (like arrays, template literals)
503
503
  if (attrStr.includes('\n')) {
504
504
  const attrLines = attrStr.split('\n');
505
505
  lines.push(`${indent}${attrLines[0]}`);
506
+ // Check if this is a template literal expression (backtick string)
507
+ // Template literal content has meaningful indentation that must be preserved
508
+ const isTemplateLiteral = this.shouldPreserveTemplateLiteral() && attrLines[0].includes('={`');
506
509
  // Add subsequent lines with additional indentation for expression content
507
510
  for (let i = 1; i < attrLines.length; i++) {
508
- // Check if this line is part of the expression or the closing
509
511
  const line = attrLines[i];
510
- if (line.trim().endsWith(']}') || line.trim() === ']}') {
512
+ if (isTemplateLiteral) {
513
+ // Preserve original indentation inside template literals
514
+ lines.push(line);
515
+ }
516
+ else if (line.trim().endsWith(']}') || line.trim() === ']}') {
511
517
  // Closing of array expression
512
518
  lines.push(`${indent}${line.trim()}`);
513
519
  }
@@ -561,6 +567,12 @@ export class HybridFormatter {
561
567
  }
562
568
  return lines.join('\n');
563
569
  }
570
+ /**
571
+ * Check if template literal indentation should be preserved based on settings.
572
+ */
573
+ shouldPreserveTemplateLiteral() {
574
+ return this.settings.formatMultiLineJsx.preserveTemplateLiteralIndent !== false;
575
+ }
564
576
  getAttributeString(attr, originalText) {
565
577
  if (!attr || !attr.name)
566
578
  return '';
@@ -573,7 +585,20 @@ export class HybridFormatter {
573
585
  else if (attr.value && attr.value.type === 'mdxJsxAttributeValueExpression') {
574
586
  // Expression value
575
587
  const exprValue = this.extractExpressionValue(attr.value);
576
- if (exprValue) {
588
+ // For template literals, prefer extracting from original text to preserve
589
+ // internal indentation (AST normalizes/strips leading whitespace)
590
+ if (this.shouldPreserveTemplateLiteral() &&
591
+ exprValue &&
592
+ exprValue.trimStart().startsWith('`')) {
593
+ const extracted = this.extractAttributeExpression(attr.name, originalText);
594
+ if (extracted) {
595
+ result = extracted;
596
+ }
597
+ else {
598
+ result += `={${exprValue}}`;
599
+ }
600
+ }
601
+ else if (exprValue) {
577
602
  result += `={${exprValue}}`;
578
603
  }
579
604
  else {
@@ -893,16 +918,69 @@ export class HybridFormatter {
893
918
  }
894
919
  });
895
920
  }
921
+ /**
922
+ * Pre-process YAML text to fix values that would cause parsing failures
923
+ * or silent data corruption. Detects unquoted values containing special
924
+ * YAML characters and wraps them in double quotes.
925
+ */
926
+ preprocessYamlForParsing(yamlText) {
927
+ const lines = yamlText.split('\n');
928
+ const result = [];
929
+ for (const line of lines) {
930
+ // Match a YAML mapping entry: optional indent, key, colon, space, value
931
+ // Keys must start with a word char, may contain word chars, dots, hyphens
932
+ const match = line.match(/^(\s*)([\w][\w.-]*):\s+(.+)$/);
933
+ if (match) {
934
+ const [, indent, key, value] = match;
935
+ const trimmedValue = value.trim();
936
+ // Skip if already quoted
937
+ if ((trimmedValue.startsWith('"') && trimmedValue.endsWith('"')) ||
938
+ (trimmedValue.startsWith("'") && trimmedValue.endsWith("'"))) {
939
+ result.push(line);
940
+ continue;
941
+ }
942
+ // Skip if the value is a flow sequence [...] or flow mapping {...}
943
+ if ((trimmedValue.startsWith('[') && trimmedValue.endsWith(']')) ||
944
+ (trimmedValue.startsWith('{') && trimmedValue.endsWith('}'))) {
945
+ result.push(line);
946
+ continue;
947
+ }
948
+ // Skip block scalar indicators (>, |, >-, |-, >+, |+)
949
+ if (/^[|>][-+]?$/.test(trimmedValue)) {
950
+ result.push(line);
951
+ continue;
952
+ }
953
+ const needsQuoting = trimmedValue.includes(': ') ||
954
+ trimmedValue.includes(' #') ||
955
+ /^[!&*%@`]/.test(trimmedValue);
956
+ if (needsQuoting) {
957
+ const escaped = trimmedValue.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
958
+ result.push(`${indent}${key}: "${escaped}"`);
959
+ continue;
960
+ }
961
+ }
962
+ result.push(line);
963
+ }
964
+ return result.join('\n');
965
+ }
896
966
  collectYamlFormatOperations(operations) {
897
967
  const yamlSettings = this.settings.formatYamlFrontmatter;
898
968
  visit(this.ast, (node) => {
899
969
  if (node.type === 'yaml' && node.position) {
900
970
  const yamlNode = node;
901
971
  try {
902
- // Parse the YAML content
903
- const parsed = yaml.load(yamlNode.value);
904
- // Format it back with proper formatting
972
+ let yamlToParse = yamlNode.value;
973
+ // Pre-process YAML to fix unsafe values (e.g., unquoted colons)
974
+ if (yamlSettings.fixUnsafeValues !== false) {
975
+ yamlToParse = this.preprocessYamlForParsing(yamlToParse);
976
+ }
977
+ // Parse the YAML content using JSON_SCHEMA to prevent silent
978
+ // data corruption (e.g., dates parsed as Date objects, octals)
979
+ const parsed = yaml.load(yamlToParse, { schema: yaml.JSON_SCHEMA });
980
+ // Format it back with proper formatting using JSON_SCHEMA
981
+ // to preserve string representations (dates, etc.)
905
982
  const formatted = yaml.dump(parsed, {
983
+ schema: yaml.JSON_SCHEMA,
906
984
  indent: yamlSettings.indent || 2,
907
985
  lineWidth: yamlSettings.lineWidth || 100,
908
986
  quotingType: (yamlSettings.quotingType || '"'),
package/dist/settings.js CHANGED
@@ -15,6 +15,8 @@ export const formatterSettings = {
15
15
  indentSize: 2,
16
16
  // Components to ignore (preserve their formatting completely)
17
17
  ignoreComponents: [],
18
+ // Preserve indentation inside template literal JSX attributes (html={`...`}, css={`...`})
19
+ preserveTemplateLiteralIndent: true,
18
20
  },
19
21
  // Rule 3: Format all HTML blocks within MDX using Prettier
20
22
  formatHtmlBlocksInMdx: {
@@ -60,6 +62,7 @@ export const formatterSettings = {
60
62
  quotingType: '"', // Quote type for strings that need quoting: '"' or "'"
61
63
  forceQuotes: false, // Force quotes on all string values
62
64
  noCompatMode: true, // Use YAML 1.2 spec (not 1.1)
65
+ fixUnsafeValues: true, // Pre-process YAML to quote values containing special characters like colons
63
66
  },
64
67
  // Rule 8: Preserve Docusaurus admonitions
65
68
  preserveAdmonitions: {
package/dist/types.d.ts CHANGED
@@ -76,6 +76,7 @@ export interface FormatMultiLineJsxSetting {
76
76
  indentSize: number;
77
77
  indentType?: string;
78
78
  ignoreComponents: string[];
79
+ preserveTemplateLiteralIndent: boolean;
79
80
  }
80
81
  export interface FormatHtmlBlocksInMdxSetting {
81
82
  enabled: boolean;
@@ -111,6 +112,7 @@ export interface FormatYamlFrontmatterSetting {
111
112
  quotingType: string;
112
113
  forceQuotes: boolean;
113
114
  noCompatMode: boolean;
115
+ fixUnsafeValues: boolean;
114
116
  }
115
117
  export interface PreserveAdmonitionsSetting {
116
118
  enabled: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/mdx-formatter",
3
- "version": "0.2.0",
3
+ "version": "0.4.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,7 @@
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",
36
37
  "doc:start": "pnpm --dir doc start",
37
38
  "prepare": "husky",
38
39
  "prepublishOnly": "tsc && vitest run"