refresh gitignore
This commit is contained in:
@@ -1,15 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
生成字体清单文件
|
||||
生成字体清单文件(稳定 ID 版本)
|
||||
扫描 fonts/ 目录下的所有字体文件,同时生成:
|
||||
1. frontend/public/fonts.json
|
||||
2. miniprogram/assets/fonts.json
|
||||
3. miniprogram/assets/fonts.js
|
||||
|
||||
ID 分配规则:
|
||||
1. 已存在字体(按 relativePath 识别)保持原 ID 不变。
|
||||
2. 新增字体按“上次游标”分配新 ID。
|
||||
3. ID 从 0001 递增到 10000,之后循环分配(跳过当前正在使用的 ID)。
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
FONT_DIR = Path('fonts')
|
||||
WEB_FONTS_JSON = Path('frontend/public/fonts.json')
|
||||
MP_FONTS_JSON = Path('miniprogram/assets/fonts.json')
|
||||
MP_FONTS_JS = Path('miniprogram/assets/fonts.js')
|
||||
ID_STATE_FILE = Path('scripts/font-id-state.json')
|
||||
|
||||
ID_MIN = 1
|
||||
ID_MAX = 10000
|
||||
|
||||
|
||||
def format_font_id(num):
|
||||
return str(num).zfill(4)
|
||||
|
||||
|
||||
def parse_font_id(raw):
|
||||
try:
|
||||
value = int(str(raw).strip())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if ID_MIN <= value <= ID_MAX:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def increment_id(num):
|
||||
return ID_MIN if num >= ID_MAX else num + 1
|
||||
|
||||
|
||||
def normalize_relative_path(raw_path):
|
||||
if not raw_path:
|
||||
return None
|
||||
|
||||
text = str(raw_path).strip()
|
||||
if not text:
|
||||
return None
|
||||
|
||||
parsed = urlparse(text)
|
||||
if parsed.scheme and parsed.netloc:
|
||||
text = parsed.path
|
||||
|
||||
text = unquote(text)
|
||||
text = text.split('?', 1)[0].split('#', 1)[0]
|
||||
text = text.replace('\\', '/')
|
||||
|
||||
if '/fonts/' in text:
|
||||
text = text.split('/fonts/', 1)[1]
|
||||
elif text.startswith('fonts/'):
|
||||
text = text[len('fonts/'):]
|
||||
elif text.startswith('/fonts'):
|
||||
text = text[len('/fonts'):]
|
||||
|
||||
text = text.lstrip('/')
|
||||
return text or None
|
||||
|
||||
|
||||
def load_manifest_id_map(manifest_file):
|
||||
mapping = {}
|
||||
if not manifest_file.exists():
|
||||
return mapping
|
||||
|
||||
try:
|
||||
with open(manifest_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return mapping
|
||||
|
||||
if not isinstance(data, list):
|
||||
return mapping
|
||||
|
||||
for item in data:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
relative_path = normalize_relative_path(
|
||||
item.get('path') or item.get('relativePath')
|
||||
)
|
||||
id_num = parse_font_id(item.get('id'))
|
||||
if relative_path and id_num:
|
||||
mapping[relative_path] = format_font_id(id_num)
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
def load_id_state(state_file):
|
||||
default_state = {
|
||||
'version': 1,
|
||||
'nextId': ID_MIN,
|
||||
'pathToId': {},
|
||||
}
|
||||
|
||||
if not state_file.exists():
|
||||
return default_state
|
||||
|
||||
try:
|
||||
with open(state_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return default_state
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return default_state
|
||||
|
||||
path_to_id = {}
|
||||
raw_map = data.get('pathToId')
|
||||
if isinstance(raw_map, dict):
|
||||
for path_key, raw_id in raw_map.items():
|
||||
relative_path = normalize_relative_path(path_key)
|
||||
id_num = parse_font_id(raw_id)
|
||||
if relative_path and id_num:
|
||||
path_to_id[relative_path] = format_font_id(id_num)
|
||||
|
||||
next_id = parse_font_id(data.get('nextId')) or ID_MIN
|
||||
|
||||
return {
|
||||
'version': 1,
|
||||
'nextId': next_id,
|
||||
'pathToId': path_to_id,
|
||||
}
|
||||
|
||||
|
||||
def save_id_state(state, state_file):
|
||||
state_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
normalized_map = dict(sorted(state['pathToId'].items(), key=lambda x: x[0]))
|
||||
data = {
|
||||
'version': 1,
|
||||
'nextId': state['nextId'],
|
||||
'pathToId': normalized_map,
|
||||
}
|
||||
with open(state_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
f.write('\n')
|
||||
|
||||
|
||||
def bootstrap_state_mappings(state):
|
||||
# 优先级:state 文件 > 小程序现有清单 > web 现有清单
|
||||
for manifest_path in (MP_FONTS_JSON, WEB_FONTS_JSON):
|
||||
mapping = load_manifest_id_map(manifest_path)
|
||||
for relative_path, font_id in mapping.items():
|
||||
state['pathToId'].setdefault(relative_path, font_id)
|
||||
|
||||
if parse_font_id(state.get('nextId')) is None:
|
||||
state['nextId'] = ID_MIN
|
||||
|
||||
if state['nextId'] == ID_MIN and state['pathToId']:
|
||||
max_id = max(parse_font_id(v) for v in state['pathToId'].values())
|
||||
state['nextId'] = increment_id(max_id)
|
||||
|
||||
|
||||
def allocate_new_id(start_id, used_ids):
|
||||
candidate = start_id
|
||||
for _ in range(ID_MAX):
|
||||
if candidate not in used_ids:
|
||||
return candidate, increment_id(candidate)
|
||||
candidate = increment_id(candidate)
|
||||
raise RuntimeError('可用 ID 已耗尽(0001-10000 全部被当前字体占用)')
|
||||
|
||||
def scan_fonts(font_dir='fonts'):
|
||||
"""扫描字体目录,返回字体信息列表"""
|
||||
@@ -45,14 +206,48 @@ def scan_fonts(font_dir='fonts'):
|
||||
|
||||
fonts.append(font_info)
|
||||
|
||||
# 统一排序后分配 4 位数字 id(0001、0002...)
|
||||
fonts = sorted(fonts, key=lambda x: (x['category'], x['name'], x['filename']))
|
||||
for index, font in enumerate(fonts, start=1):
|
||||
font['id'] = f"{index:04d}"
|
||||
|
||||
# 固定顺序,保证分配冲突时结果稳定
|
||||
fonts = sorted(fonts, key=lambda x: (x['category'], x['name'], x['filename'], x['relativePath']))
|
||||
return fonts
|
||||
|
||||
|
||||
def assign_stable_ids(fonts):
|
||||
state = load_id_state(ID_STATE_FILE)
|
||||
bootstrap_state_mappings(state)
|
||||
|
||||
cursor = parse_font_id(state.get('nextId')) or ID_MIN
|
||||
used_ids = set()
|
||||
new_count = 0
|
||||
|
||||
for font in fonts:
|
||||
relative_path = font['relativePath']
|
||||
existing_id = state['pathToId'].get(relative_path)
|
||||
existing_num = parse_font_id(existing_id)
|
||||
|
||||
if existing_num and existing_num not in used_ids:
|
||||
assigned_num = existing_num
|
||||
else:
|
||||
if existing_num and existing_num in used_ids:
|
||||
print(
|
||||
f"警告: 发现 ID 冲突,路径 {relative_path} 原 ID {existing_id} 将重新分配。"
|
||||
)
|
||||
assigned_num, cursor = allocate_new_id(cursor, used_ids)
|
||||
state['pathToId'][relative_path] = format_font_id(assigned_num)
|
||||
new_count += 1
|
||||
|
||||
used_ids.add(assigned_num)
|
||||
font['id'] = format_font_id(assigned_num)
|
||||
|
||||
state['nextId'] = cursor
|
||||
save_id_state(state, ID_STATE_FILE)
|
||||
|
||||
return {
|
||||
'newCount': new_count,
|
||||
'nextId': format_font_id(cursor),
|
||||
'stateFile': str(ID_STATE_FILE),
|
||||
}
|
||||
|
||||
|
||||
def build_manifest(fonts, path_prefix):
|
||||
"""根据路径前缀构建对外清单"""
|
||||
prefix = f"/{str(path_prefix or '').strip('/')}"
|
||||
@@ -91,18 +286,25 @@ def write_fonts_js(fonts, output_file):
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 扫描字体(唯一来源:仓库根目录 fonts/)
|
||||
fonts = scan_fonts('fonts')
|
||||
fonts = scan_fonts(str(FONT_DIR))
|
||||
|
||||
print(f"找到 {len(fonts)} 个字体文件")
|
||||
|
||||
allocation_result = assign_stable_ids(fonts)
|
||||
print(
|
||||
f"ID 分配完成:新增 {allocation_result['newCount']} 个,"
|
||||
f"下次起始 ID {allocation_result['nextId']},"
|
||||
f"状态文件 {allocation_result['stateFile']}"
|
||||
)
|
||||
|
||||
# Web 清单:统一指向根目录 fonts
|
||||
web_fonts = build_manifest(fonts, '/fonts')
|
||||
write_fonts_json(web_fonts, 'frontend/public/fonts.json')
|
||||
write_fonts_json(web_fonts, str(WEB_FONTS_JSON))
|
||||
|
||||
# 小程序清单:同样指向根目录 fonts(与 web 共用一份字体目录)
|
||||
miniprogram_fonts = build_manifest(fonts, '/fonts')
|
||||
write_fonts_json(miniprogram_fonts, 'miniprogram/assets/fonts.json')
|
||||
write_fonts_js(miniprogram_fonts, 'miniprogram/assets/fonts.js')
|
||||
write_fonts_json(miniprogram_fonts, str(MP_FONTS_JSON))
|
||||
write_fonts_js(miniprogram_fonts, str(MP_FONTS_JS))
|
||||
|
||||
# 统计信息
|
||||
categories = {}
|
||||
|
||||
Reference in New Issue
Block a user