You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
224 lines
7.1 KiB
Python
224 lines
7.1 KiB
Python
#!/usr/bin/env vpython3
|
|
# [VPYTHON:BEGIN]
|
|
# python_version: "3.11"
|
|
# # Note: mdformat depends on mdurl and markdown-it-py, which are included above.
|
|
# wheel: <
|
|
# name: "infra/python/wheels/mdurl-py3"
|
|
# version: "version:0.1.2"
|
|
# >
|
|
# wheel: <
|
|
# name: "infra/python/wheels/markdown-it-py-py3"
|
|
# version: "version:2.2.0"
|
|
# >
|
|
# wheel: <
|
|
# name: "infra/python/wheels/mdformat-py3"
|
|
# version: "version:0.7.22"
|
|
# >
|
|
# wheel: <
|
|
# name: "infra/python/wheels/mdit-py-plugins-py3"
|
|
# version: "version:0.5.0"
|
|
# >
|
|
# wheel: <
|
|
# name: "infra/python/wheels/mdformat_frontmatter-py3"
|
|
# version: "version:2.0.10"
|
|
# >
|
|
# wheel: <
|
|
# name: "infra/python/wheels/ruamel_yaml-py3"
|
|
# version: "version:0.17.16"
|
|
# >
|
|
# [VPYTHON:END]
|
|
|
|
# Copyright 2026 The Chromium Authors
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
"""Markdown formatting wrapper for depot_tools."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import difflib
|
|
import os
|
|
import sys
|
|
from typing import Iterable, Optional
|
|
|
|
import mdformat
|
|
import setup_color
|
|
import utils
|
|
from third_party import colorama
|
|
|
|
# Internal formatting rules.
|
|
_WRAP_WIDTH = '80'
|
|
_CONFIG_FILENAME = '.style.mdformat'
|
|
|
|
|
|
def _get_colored_diff(diff: Iterable[str]) -> str:
|
|
"""Adds ANSI colors to a unified diff if the output is a TTY."""
|
|
if not setup_color.IS_TTY:
|
|
return ''.join(diff)
|
|
|
|
colored_lines = []
|
|
for line in diff:
|
|
if line.startswith('---') or line.startswith('+++'):
|
|
colored_lines.append(line)
|
|
elif line.startswith('-'):
|
|
colored_lines.append(colorama.Fore.RED + line +
|
|
colorama.Style.RESET_ALL)
|
|
elif line.startswith('+'):
|
|
colored_lines.append(colorama.Fore.GREEN + line +
|
|
colorama.Style.RESET_ALL)
|
|
elif line.startswith('@@'):
|
|
colored_lines.append(colorama.Fore.CYAN + line +
|
|
colorama.Style.RESET_ALL)
|
|
else:
|
|
colored_lines.append(line)
|
|
return ''.join(colored_lines)
|
|
|
|
|
|
def print_diff(original: str, formatted: str, filename: str) -> int:
|
|
"""Prints a unified diff between original and formatted text.
|
|
|
|
Returns:
|
|
2 if differences were found, 0 otherwise.
|
|
"""
|
|
diff = list(
|
|
difflib.unified_diff(original.splitlines(keepends=True),
|
|
formatted.splitlines(keepends=True),
|
|
fromfile=f'a/{filename}',
|
|
tofile=f'b/{filename}'))
|
|
|
|
if not diff:
|
|
return 0
|
|
|
|
sys.stdout.write(_get_colored_diff(diff))
|
|
return 2
|
|
|
|
|
|
def process_stdin(args: argparse.Namespace) -> int:
|
|
"""Handles formatting when input is provided via stdin.
|
|
|
|
This mode is primarily used for integration with tools like `jj fix`,
|
|
which operates on a "pipe model": it streams file content to the
|
|
formatter's stdin and expects the formatted result on stdout.
|
|
`jj fix` handles stacked commits by iterating over each revision in the
|
|
stack and applying the formatter to the changed files. See the "Execution
|
|
Example" in `jj help fix` for more details.
|
|
|
|
Since `jj fix` does not provide the original file path as a positional
|
|
argument when piping, we use `--assume-filename` to pass the path
|
|
separately. This allows us to perform the recursive opt-in check
|
|
(searching for `.style.mdformat`) based on the file's location in the
|
|
directory hierarchy.
|
|
"""
|
|
if args.files:
|
|
print('Error: Cannot specify files with --from-stdin.', file=sys.stderr)
|
|
return 1
|
|
|
|
original = sys.stdin.read()
|
|
|
|
# Opt-in check for stdin if a filename hint is provided.
|
|
if args.assume_filename:
|
|
if not utils.find_config_file(args.assume_filename, _CONFIG_FILENAME):
|
|
# Not opted-in. Stream original input to stdout for pipes and exit.
|
|
if not args.check and not args.diff:
|
|
sys.stdout.write(original)
|
|
return 0
|
|
|
|
try:
|
|
formatted = mdformat.text(original,
|
|
options={'wrap': int(_WRAP_WIDTH)},
|
|
extensions={'frontmatter'})
|
|
except Exception as e:
|
|
sys.stderr.write(f'Error formatting: {e}\n')
|
|
return 1
|
|
|
|
if args.diff:
|
|
return print_diff(original, formatted, args.assume_filename or 'stdin')
|
|
|
|
if args.check:
|
|
if original != formatted:
|
|
print('Markdown content is not formatted.', file=sys.stderr)
|
|
return 2
|
|
return 0
|
|
|
|
# Default filter mode: Output formatted content for pipes.
|
|
sys.stdout.write(formatted)
|
|
return 0
|
|
|
|
|
|
def process_files(args: argparse.Namespace) -> int:
|
|
"""Handles formatting for a list of files on disk."""
|
|
return_value = 0
|
|
for path in args.files:
|
|
if not os.path.exists(path):
|
|
print(f'Error: File not found: {path}', file=sys.stderr)
|
|
return_value = 1
|
|
continue
|
|
|
|
# Opt-in check: only format if a marker file is found.
|
|
if not utils.find_config_file(path, _CONFIG_FILENAME):
|
|
continue
|
|
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
original = f.read()
|
|
|
|
try:
|
|
formatted = mdformat.text(original,
|
|
options={'wrap': int(_WRAP_WIDTH)},
|
|
extensions={'frontmatter'})
|
|
except Exception as e:
|
|
print(f'Error formatting {path}: {e}', file=sys.stderr)
|
|
return_value = 1
|
|
continue
|
|
|
|
if args.diff:
|
|
ret = print_diff(original, formatted, path)
|
|
return_value = return_value or ret
|
|
elif args.check:
|
|
if original != formatted:
|
|
print(f'File "{path}" is not formatted.', file=sys.stderr)
|
|
return_value = return_value or 2
|
|
else:
|
|
if original != formatted:
|
|
with open(path, 'w', encoding='utf-8') as f:
|
|
f.write(formatted)
|
|
print(f'Formatted {path}')
|
|
|
|
if return_value == 2 and args.check:
|
|
print('\nMarkdown files are not formatted correctly.', file=sys.stderr)
|
|
print('To fix, run:', file=sys.stderr)
|
|
print(f' git cl format {" ".join(args.files)}', file=sys.stderr)
|
|
|
|
return return_value
|
|
|
|
|
|
def main() -> int:
|
|
setup_color.init()
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
'--check',
|
|
action='store_true',
|
|
help='Check formatting and return non-zero if not formatted.')
|
|
parser.add_argument(
|
|
'--diff',
|
|
action='store_true',
|
|
help='Print diff to stdout rather than modifying files.')
|
|
parser.add_argument('--from-stdin',
|
|
action='store_true',
|
|
help='Read from stdin.')
|
|
parser.add_argument('--assume-filename',
|
|
help='Filename hint when using --from-stdin.')
|
|
parser.add_argument('files', nargs='*', help='Markdown files to process.')
|
|
args = parser.parse_args()
|
|
|
|
if args.from_stdin:
|
|
return process_stdin(args)
|
|
|
|
if not args.files:
|
|
return 0
|
|
|
|
return process_files(args)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|