"""Settings for reformater.""" # pylint: disable=C0301,C0103 # flake8: noqa import logging ## get pyproject.toml settings from pathlib import Path from typing import Dict, List, Optional, Union import tomlkit import yaml from click import echo from colorama import Fore from pathspec import PathSpec from pathspec.patterns.gitwildmatch import GitWildMatchPatternError logger = logging.getLogger(__name__) def find_project_root(src: Path) -> Path: """Attempt to get the project root.""" for directory in [src, *src.resolve().parents]: if (directory / ".git").exists(): return directory if (directory / ".hg").is_dir(): return directory if (directory / "pyproject.toml").is_file(): return directory # pylint: disable=W0631 return directory def load_gitignore(root: Path) -> PathSpec: """Search upstream for a .gitignore file.""" gitignore = root / ".gitignore" git_lines: List[str] = [] if gitignore.is_file(): with gitignore.open(encoding="utf-8") as this_file: git_lines = this_file.readlines() try: return PathSpec.from_lines("gitwildmatch", git_lines) except GitWildMatchPatternError as e: echo(f"Could not parse {gitignore}: {e}", err=True) raise def find_pyproject(root: Path) -> Optional[Path]: """Search upstream for a pyproject.toml file.""" pyproject = root / "pyproject.toml" if pyproject.is_file(): return pyproject return None def find_djlint_rules(root: Path) -> Optional[Path]: """Search upstream for a pyprojec.toml file.""" rules = root / ".djlint_rules.yaml" if rules.is_file(): return rules return None def load_pyproject_settings(src: Path) -> Dict: """Load djlint config from pyproject.toml.""" djlint_content: Dict = {} pyproject_file = find_pyproject(src) if pyproject_file: content = tomlkit.parse(pyproject_file.read_text(encoding="utf8")) try: djlint_content = content["tool"]["djlint"] # type: ignore except KeyError: logger.info("No pyproject.toml found.") return djlint_content def validate_rules(rules: List) -> List: """Validate a list of linter rules. Returns valid rules.""" clean_rules = [] for rule in rules: # check for name warning = 0 name = rule["rule"].get("name", "undefined") if "name" not in rule["rule"]: warning += 1 echo(Fore.RED + "Warning: A rule is missing a name! 😢") if "patterns" not in rule["rule"]: warning += 1 echo(Fore.RED + f"Warning: Rule {name} is missing a pattern! 😢") if "message" not in rule["rule"]: warning += 1 echo(Fore.RED + f"Warning: Rule {name} is missing a message! 😢") if warning == 0: clean_rules.append(rule) return clean_rules def load_custom_rules(src: Path) -> List: """Load djlint config from pyproject.toml.""" djlint_content: List = [] djlint_rules_file = find_djlint_rules(src) if djlint_rules_file: djlint_content = yaml.load( Path(djlint_rules_file).read_text(encoding="utf8"), Loader=yaml.SafeLoader, ) return djlint_content def build_custom_blocks(custom_blocks: Union[str, None]) -> Optional[str]: """Build regex string for custom template blocks.""" if custom_blocks: return "|" + "|".join(x.strip() for x in custom_blocks.split(",")) return None class Config: """Djlint Config.""" def __init__( self, src: str, ignore: Optional[str] = None, extension: Optional[str] = None, indent: Optional[int] = None, quiet: bool = False, profile: Optional[str] = None, require_pragma: bool = False, reformat: bool = False, check: bool = False, lint: bool = False, use_gitignore: bool = False, ): self.reformat = reformat self.check = check self.lint = lint self.stdin = "-" in src project_root = find_project_root(Path(src)) djlint_settings = load_pyproject_settings(project_root) self.gitignore = load_gitignore(project_root) # custom configuration options self.use_gitignore: bool = use_gitignore or djlint_settings.get( "use_gitignore", False ) self.extension: str = str(extension or djlint_settings.get("extension", "html")) self.quiet: bool = quiet or djlint_settings.get("quiet", False) self.require_pragma: bool = ( require_pragma or str(djlint_settings.get("require_pragma", "false")).lower() == "true" ) self.custom_blocks: str = str( build_custom_blocks(djlint_settings.get("custom_blocks")) or "" ) # ignore is based on input and also profile self.ignore: str = str(ignore or djlint_settings.get("ignore", "")) # codes to exclude profile_dict: Dict[str, List[str]] = { "django": ["J", "N", "M"], "jinja": ["D", "N", "M"], "nunjucks": ["D", "J", "M"], "handlebars": ["D", "J", "N"], "golang": ["D", "J", "N", "M"], } self.profile_code: List[str] = profile_dict.get( str(profile or djlint_settings.get("profile", "all")).lower(), [] ) self.profile: str = str( profile or djlint_settings.get("profile", "all") ).lower() # load linter rules rule_set = validate_rules( yaml.load( (Path(__file__).parent / "rules.yaml").read_text(encoding="utf8"), Loader=yaml.SafeLoader, ) + load_custom_rules(project_root) ) self.linter_rules = list( filter( lambda x: x["rule"]["name"] not in self.ignore.split(",") and x["rule"]["name"][0] not in self.profile_code and self.profile not in x["rule"].get("exclude", []), rule_set, ) ) # base options default_indent = 4 if not indent: try: indent = int(djlint_settings.get("indent", default_indent)) except ValueError: echo( Fore.RED + f"Error: Invalid pyproject.toml indent value {djlint_settings['indent']}" ) indent = default_indent self.indent: str = indent * " " default_exclude: str = r""" \.venv | venv/ | \.tox | \.eggs | \.git | \.hg | \.mypy_cache | \.nox | \.svn | \.bzr | _build/ | buck-out/ | build/ | dist/ | \.pants\.d | \.direnv | node_modules/ | __pypackages__ """ self.exclude: str = djlint_settings.get("exclude", default_exclude) extend_exclude: str = djlint_settings.get("extend_exclude", "") if extend_exclude: self.exclude += r" | " + r" | ".join( x.strip() for x in extend_exclude.split(",") ) # add blank line after load tags self.blank_line_after_tag: Optional[str] = djlint_settings.get( "blank_line_after_tag", None ) # contents of tags will not be formatted self.ignored_block_opening: str = r""" | <(script|style).*?(?=(\)) # html comment | .*? # django/jinja | {\#\s*djlint\:off\s*\#}.*?{\#\s*djlint\:on\s*\#} | {%\s*comment\s*%\}\s*djlint\:off\s*\{%\s*endcomment\s*%\}.*?{%\s*comment\s*%\}\s*djlint\:on\s*\{%\s*endcomment\s*%\} # nunjucks | {\#\s*djlint\:off\s*\#}.*?{\#\s*djlint\:on\s*\#} # handlebars | {{!--\s*djlint\:off\s*--}}.*?{{!--\s*djlint\:on\s*--}} # golang | {{-?\s*/\*\s*djlint\:off\s*\*/\s*-?}}.*?{{-?\s*/\*\s*djlint\:on\s*\*/\s*-?}} | | <\?php.*?\?> # | {\%[ ]trans[ ][^}]*?\%} | {%[ ]+?comment[ ]+?[^(?:%})]*?%}.*?{%[ ]+?endcomment[ ]+?%} """ self.ignored_inline_blocks: str = r""" | {\*.*?\*} | {\#.*?\#} | <\?php.*?\?> | {%[ ]+?comment[ ]+?[^(?:%})]*?%}.*?{%[ ]+?endcomment[ ]+?%} """ self.optional_single_line_html_tags: str = r""" button | a | h1 | h2 | h3 | h4 | h5 | h6 | td | th | strong | small | em | icon | span | title | link | path | label | div | li | script | style """ self.always_self_closing_html_tags: str = r""" link | img | meta | source | br | input | hr """ self.optional_single_line_template_tags: str = r""" if | for | block | with """ self.break_html_tags: str = ( r""" html | head | body | div | a | nav | ul | ol | dl | dd | dt | li | table | thead | tbody | tr | th | td | blockquote | select | form | option | optgroup | fieldset | legend | label | header | cache | main | section | aside | footer | figure | figcaption | video | span | p | g | svg | h\d | button | path | picture | script | style | details | summary | """ + self.always_self_closing_html_tags + """ """ )