Load business config as JSONC (comments + trailing commas) via an in-repo stripper then stdlib json — no new deps. Default path prefers config.jsonc, falls back to legacy config.json. Auth write-back targets only sibling secrets.json so JSONC comments are never wiped; secrets overlay merges on load. Examples, compose, Docker, docs, favsort and tools all share the same loader. Phase A only — no DB settings migration.
108 lines
2.9 KiB
Python
108 lines
2.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""JSONC(JSON with Comments)加载。
|
||
|
||
选择**自研轻量剥离**而非引入第三方依赖:本仓库运行依赖目前只有 ``httpx``,
|
||
配置加载属于启动/热更新热路径,JSONC 只需注释与尾逗号。实现按字符扫描,
|
||
在字符串字面量内保留 ``//``、``/*``、引号转义,避免误伤。
|
||
|
||
支持:
|
||
- ``//`` 行注释、``/* ... */`` 块注释
|
||
- 对象/数组中的尾逗号
|
||
- UTF-8 BOM
|
||
不支持 JSON5 扩展(无引号键、单引号、十六进制、``Infinity`` 等)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from typing import Any
|
||
|
||
|
||
def strip_jsonc(text: str) -> str:
|
||
"""去掉 JSONC 注释与尾逗号,返回可交给 ``json.loads`` 的文本。"""
|
||
if text.startswith("\ufeff"):
|
||
text = text[1:]
|
||
out: list[str] = []
|
||
i = 0
|
||
n = len(text)
|
||
in_string = False
|
||
escape = False
|
||
while i < n:
|
||
ch = text[i]
|
||
if in_string:
|
||
out.append(ch)
|
||
if escape:
|
||
escape = False
|
||
elif ch == "\\":
|
||
escape = True
|
||
elif ch == '"':
|
||
in_string = False
|
||
i += 1
|
||
continue
|
||
if ch == '"':
|
||
in_string = True
|
||
out.append(ch)
|
||
i += 1
|
||
continue
|
||
if ch == "/" and i + 1 < n:
|
||
nxt = text[i + 1]
|
||
if nxt == "/":
|
||
i += 2
|
||
while i < n and text[i] not in "\r\n":
|
||
i += 1
|
||
continue
|
||
if nxt == "*":
|
||
i += 2
|
||
while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
|
||
i += 1
|
||
i = min(i + 2, n)
|
||
continue
|
||
out.append(ch)
|
||
i += 1
|
||
|
||
stripped = "".join(out)
|
||
# 去掉尾逗号:`,}` / `,]`(可夹空白)
|
||
cleaned: list[str] = []
|
||
i = 0
|
||
m = len(stripped)
|
||
in_string = False
|
||
escape = False
|
||
while i < m:
|
||
ch = stripped[i]
|
||
if in_string:
|
||
cleaned.append(ch)
|
||
if escape:
|
||
escape = False
|
||
elif ch == "\\":
|
||
escape = True
|
||
elif ch == '"':
|
||
in_string = False
|
||
i += 1
|
||
continue
|
||
if ch == '"':
|
||
in_string = True
|
||
cleaned.append(ch)
|
||
i += 1
|
||
continue
|
||
if ch == ",":
|
||
j = i + 1
|
||
while j < m and stripped[j] in " \t\r\n":
|
||
j += 1
|
||
if j < m and stripped[j] in "}]":
|
||
i += 1
|
||
continue
|
||
cleaned.append(ch)
|
||
i += 1
|
||
return "".join(cleaned)
|
||
|
||
|
||
def loads(text: str) -> Any:
|
||
"""解析 JSONC 文本。"""
|
||
return json.loads(strip_jsonc(text))
|
||
|
||
|
||
def load(path: str | bytes, *, encoding: str = "utf-8") -> Any:
|
||
"""从路径读取并解析 JSONC / JSON。"""
|
||
with open(path, encoding=encoding) as fh:
|
||
return loads(fh.read())
|