Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ __pycache__
/coverage.xml
alliance_auth.sqlite3

/*.min.css
/*.min.css.map
/*.min.js
/*.min.js.map

/sovtimer/locale/en/
.esi-compatibility-date
.make/myauth-path
Expand Down
4 changes: 4 additions & 0 deletions .make/conf.d/npm.mk
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ npm-audit-fix:
# Run a specific npm script
.PHONY: npm-run
npm-run:
@if [ -z "$(script)" ]; then \
echo "Error: Please specify the npm script to run using 'make npm-run script=<script>'."; \
exit 1; \
fi
@echo "Running npm script…"
@npm run $(script)

Expand Down
12 changes: 6 additions & 6 deletions .make/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ password =

[Development]
base_dir = .
test_db_name = test_alliance_auth_development
test_db_name = test_${DB:name}

[Django]
myauth_path = ../myauth
translation_directory = sovtimer/locale
translation_template = sovtimer/locale/django.pot
translation_directory = ${General:package}/locale
translation_template = ${Django:translation_directory}/django.pot
translation_file_relative_path = LC_MESSAGES/django.po

[Git]
git_repository = https://github.com/ppfeufer/aa-sov-timer
git_repository_issues = https://github.com/ppfeufer/aa-sov-timer/issues
git_repository = https://github.com/ppfeufer/${General:appname}
git_repository_issues = ${Git:git_repository}/issues

[Python]
executable = ../venv/bin/python
Expand All @@ -49,4 +49,4 @@ venv_path = ../venv
[Weblate]
base_url = https://weblate.ppfeufer.de
project_slug = alliance-auth-apps
component_slug = aa-sovereignty-timer
component_slug = ${General:appname}
179 changes: 179 additions & 0 deletions .make/parse_ini_interpolate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/usr/bin/env python3

"""
This script parses INI files, normalizes the section and key names, and resolves interpolations in the values. It can output the results in different formats based on the specified mode.
"""

# Standard Library
import re
import sys
from pathlib import Path


def normalize_name(section, key):
"""
Normalize the section and key names to a consistent format.

:param section: The section name from the INI file
:param key: The key name from the INI file
:return: A normalized name in the format SECTION__KEY, uppercased and with non-alphanumeric characters replaced by underscores
"""

if section is None or section == "":
name = key
else:
name = f"{section}__{key}"

name = name.upper()
name = re.sub(r"[^A-Z0-9_]", "_", name)

return name


def parse_files(files):
"""
Parse the given INI files and return a dictionary of normalized names to values, along with the order of the names.

:param files: A list of file paths to INI files
:return: A tuple containing a dictionary of normalized names to values and a list of names in the order they were parsed
"""

vals = {}
order = []
section = ""

for fp in files:
p = Path(fp)

if not p.exists():
continue

with p.open(encoding="utf-8") as f:
for line in f:
s = line.rstrip("\n")
# section header
m = re.match(r"^\s*\[([^]]+)]\s*$", s)

if m:
section = m.group(1).strip()

continue

# skip comments or empty
if re.match(r"^\s*[#;]", s) or not re.search(r"=", s):
continue

# split on first '='
key, val = re.split(r"=", s, maxsplit=1)
key = key.strip()
val = val.strip()
name = normalize_name(section, key)
vals[name] = val
order.append(name)

return vals, order


def resolve_interpolations(vals, order, max_iters=10):
"""
Resolve interpolations in the values of the given dictionary.

:param vals: A dictionary of normalized names to values
:param order: A list of names in the order they were parsed
:param max_iters: The maximum number of iterations to resolve interpolations
:return: The dictionary of values with interpolations resolved
"""

pattern = re.compile(r"\$\{([^}:]+):?([^}]*)}")

for _ in range(max_iters):
changed = False

for name in order:
v = vals.get(name, "")

def _repl(m):
"""
Replacement function for regex substitution.

:param m: The regex match object
:return: The replacement string
"""

sec = m.group(1) or ""
key = m.group(2) or ""

if key == "":
# If no colon provided, treat whole as key and empty section
ref = normalize_name("", sec)
else:
ref = normalize_name(sec, key)
return vals.get(ref, "")

newv = pattern.sub(_repl, v)

if newv != v:
vals[name] = newv
changed = True

if not changed:
break

return vals


def main(argv):
"""
Main function to parse INI files and resolve interpolations.

:param argv: A list of command-line arguments
:return: None
"""

mode = "make"
files = []
i = 1

while i < len(argv):
a = argv[i]

if a.startswith("--mode="):
mode = a.split("=", 1)[1]
else:
files.append(a)

i += 1

vals, order = parse_files(files)
vals = resolve_interpolations(vals, order)
out_lines = []

if mode == "names":
seen = set()

for name in order:
if name in seen:
continue

seen.add(name)
out_lines.append(name)

sys.stdout.write("\n".join(out_lines) + "\n")

return

for name in order:
v = vals.get(name, "")

if mode == "make":
# Escape $ for Makefile (write $$ so that Makefile sees a single $)
v = v.replace("$", "$$")
out_lines.append(f"{name} := {v}")
else:
out_lines.append(f"{name}={v}")

sys.stdout.write("\n".join(out_lines) + "\n")


if __name__ == "__main__":
main(sys.argv)
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ ParsedConfigFiles := $(ConfigFile) $(wildcard $(ConfigFileOverride))
# Extract all config values from $(ConfigFile) and export them as Makefile variables
ifneq ($(wildcard $(ConfigFile)),)
TMPFILE := $(shell mkdir -p $(dir $(ConfigFile)) && mktemp $(dir $(ConfigFile))/make_vars.XXXXXX)
$(shell awk -F= '/^\[/{gsub(/^^\[|\]$$/, "", $$0); section=$$0; next} /^[^#;].*=/ { key=$$1; val=$$0; sub(/^[^=]*=/, "", val); gsub(/^[ \t]+|[ \t]+$$/, "", val); gsub(/^[ \t]+|[ \t]+$$/, "", key); if(section=="") name=toupper(key); else name=toupper(section"__"key); gsub(/[^A-Z0-9_]/, "_", name); gsub(/[$$]/, "$$$$", val); printf "%s := %s\n", name, val }' $(ParsedConfigFiles) > $(TMPFILE))
$(shell /usr/bin/env python3 .make/parse_ini_interpolate.py --mode=make $(ParsedConfigFiles) > $(TMPFILE))
include $(TMPFILE)
$(shell rm -f $(TMPFILE))

# Also capture the list of variable NAMES (for show-config output).
ConfigVars := $(shell awk -F= '/^\[/{gsub(/^^\[|\]$$/, "", $$0); section=$$0; next} /^[^#;].*=/ { key=$$1; gsub(/^[ \t]+|[ \t]+$$/, "", key); if(section=="") name=toupper(key); else name=toupper(section"__"key); gsub(/[^A-Z0-9_]/, "_", name); print name }' $(ParsedConfigFiles) | sort -u)
# Use the Python parser to normalize names (keeps behavior consistent with interpolation)
ConfigVars := $(shell /usr/bin/env python3 .make/parse_ini_interpolate.py --mode=names $(ParsedConfigFiles) | sort -u)
else
# Notify the user that no config file was found.
$(error Config file '$(ConfigFile)' not found. To configure the project, create '$(ConfigFile)')
Expand Down Expand Up @@ -168,9 +169,9 @@ show-parsed-config-files:
override="$(ConfigFileOverride)"; \
base_tmp=$$(mktemp); \
ovr_tmp=$$(mktemp); \
awk -F= '/^\[/ {gsub(/^^\[|\]$$/, "", $$0); section=$$0; next} /^[^#;].*=/ { key=$$1; val=$$0; sub(/^[^=]*=/, "", val); gsub(/^[ \t]+|[ \t]+$$/, "", val); gsub(/^[ \t]+|[ \t]+$$/, "", key); if(section=="") name=toupper(key); else name=toupper(section"__"key); gsub(/[^A-Z0-9_]/, "_", name); printf "%s=%s\n", name, val }' "$$base" > "$$base_tmp"; \
/usr/bin/env python3 .make/parse_ini_interpolate.py --mode=pair "$$base" > "$$base_tmp"; \
if [ -f "$$override" ]; then \
awk -F= '/^\[/ {gsub(/^^\[|\]$$/, "", $$0); section=$$0; next} /^[^#;].*=/ { key=$$1; val=$$0; sub(/^[^=]*=/, "", val); gsub(/^[ \t]+|[ \t]+$$/, "", val); gsub(/^[ \t]+|[ \t]+$$/, "", key); if(section=="") name=toupper(key); else name=toupper(section"__"key); gsub(/[^A-Z0-9_]/, "_", name); printf "%s=%s\n", name, val }' "$$override" > "$$ovr_tmp"; \
/usr/bin/env python3 .make/parse_ini_interpolate.py --mode=pair "$$override" > "$$ovr_tmp"; \
if [ -s "$$ovr_tmp" ]; then \
echo "$(TEXT_UNDERLINE)Overridden variables:$(TEXT_RESET)"; \
base_sorted="$$base_tmp.sorted"; \
Expand Down
51 changes: 39 additions & 12 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,61 @@
import globals from 'globals'; // Import the globals package
import js from '@eslint/js'; // Import the ESLint JavaScript configuration
import { defineConfig } from 'eslint/config'; // Import the defineConfig function from ESLint
import {defineConfig} from 'eslint/config'; // Import the defineConfig function from ESLint
import stylistic from '@stylistic/eslint-plugin'; // Import the Stylistic ESLint plugin

export default defineConfig([
{
plugins: {
'@stylistic': stylistic
},
languageOptions: {
globals: {
...globals.browser,
...globals.commonjs,
...globals.jquery,
...globals.jquery
},
parserOptions: {
...js.configs.recommended.parserOptions,
ecmaVersion: 'latest',
ecmaFeatures: {
impliedStrict: true,
impliedStrict: true
},
sourceType: 'module',
},
sourceType: 'module'
}
},
rules: {
...js.configs.recommended.rules,
indent: ['error', 4, {
'SwitchCase': 1
...stylistic.configs.recommended.rules,
// https://eslint.style/rules/arrow-parens
'@stylistic/arrow-parens': ['error', 'always'],
// https://eslint.style/rules/brace-style
'@stylistic/brace-style': ['error', '1tbs', {
allowSingleLine: false
}],
quotes: ['error', 'single', {
avoidEscape: true,
allowTemplateLiterals: true
// https://eslint.style/rules/comma-dangle
'@stylistic/comma-dangle': ['error', 'never'],
// https://eslint.style/rules/indent
'@stylistic/indent': ['error', 4, {
SwitchCase: 1
}],
semi: [2, 'always'],
},
// https://eslint.style/rules/indent-binary-ops
'@stylistic/indent-binary-ops': ['error', 4],
// https://eslint.style/rules/object-curly-spacing
'@stylistic/object-curly-spacing': ['error', 'never'],
// https://eslint.style/rules/quotes
'@stylistic/quotes': ['error', 'single', {
allowTemplateLiterals: 'avoidEscape',
avoidEscape: true
}],
// https://eslint.style/rules/semi
'@stylistic/semi': ['error', 'always'],
// https://eslint.style/rules/space-before-function-paren
'@stylistic/space-before-function-paren': ['error', {
anonymous: 'always',
asyncArrow: 'always',
catch: 'always',
named: 'always'
}]
}
}
]);
Loading
Loading