Compare commits

..

7 Commits

Author SHA1 Message Date
Nanaloveyuki
80e4a4d02f 🚀 发布了Beta0.1.2测试版 2025-07-29 14:20:34 +08:00
Nanaloveyuki
eed6144970 🐛 修复了时间戳格式错误的问题 2025-07-29 14:17:45 +08:00
Nanaloveyuki
796a8a82f6 🎨 改进了对占位符和正则表达式的处理 2025-07-29 14:17:22 +08:00
Nanaloveyuki
89bc381235 📝 更新了文档,使其支持占位符等 2025-07-29 14:16:38 +08:00
Nanaloveyuki
89cb26b9b3 message富文本功能基本测试完成 2025-07-28 21:21:46 +08:00
Nanaloveyuki
9b5d7cdfad 增加了message参数对于md/html部分富文本语法的支持和改进了HEX着色 2025-07-28 21:21:21 +08:00
Nanaloveyuki
d5f157bdf3 🧑‍💻 增加快速打包 2025-07-28 17:58:32 +08:00
16 changed files with 467 additions and 170 deletions

View File

@@ -14,8 +14,38 @@ pip install logiliteal
**支持高可扩展的样式** **支持高可扩展的样式**
- 支持使用HEX十六进制颜色代码`<#ffffff>text</>`渲染颜色 - 支持使用HEX十六进制颜色代码`<#ffffff>text</>`渲染颜色
- 支持使用占位符`{placeholder}`渲染变量(可手动扩展) - 支持使用占位符`{placeholder}`渲染变量(可手动扩展)
- 支持部分Html或Markdown语法(如`<b>text</b>`)
- 支持自定义日志格式和日志颜色 - 支持自定义日志格式和日志颜色
Html语法支持:
- `<b>text</b>` 加粗
- `<i>text</i>` 斜体
- `<u>text</u>` 下划线
- `<s>text</s>` 删除线
- `<c>` 清除颜色
- `<br>` 换行
- `</>` Html万用闭合
> **注意!Html嵌套可能会有问题, 不建议过多嵌套**
Markdown语法支持:
- `**text**` 加粗
- `*text*` 斜体
- `__text__` 下划线
- `~~text~~` 删除线
- `[text](url)` 链接
> **注意!Html和Markdown语法虽然可以同时使用,但是不保证所有语法都能正常工作, 建议只使用其中一种**
> **目前语法解析属于测试阶段,欢迎反馈或者提出Pr**
- 目前支持的Html标签: `<b>`, `<i>`, `<u>`, `<s>`, `<c>`, `<br>`, `</>`
- 目前支持的Markdown语法: `**`, `*`, `__`, `~~`, `[text](url)`
- 目前支持的变量:
- `{asctime}` 对应日志完整时间(`config.asctime`)
- `{time}` 对应日志简略时间(`config.time`)
- `{weekday}` 对应日志星期(`config.weekday`)
- `{date}` 对应日志日期(`config.date`)
**支持的Python版本** **支持的Python版本**
- Python 3.13.5 - Python 3.13.5
- Python 3.13.4 - Python 3.13.4

View File

@@ -7,7 +7,7 @@
"enable_console": true, "enable_console": true,
"enable_file": true, "enable_file": true,
"console_color": true, "console_color": true,
"console_level": "INFO", "console_level": "DEBUG",
"console_format": "{time} {levelname} | {prefix}{message}", "console_format": "{time} {levelname} | {prefix}{message}",
"console_prefix": "Auto", "console_prefix": "Auto",
"console_encoding": "utf-8", "console_encoding": "utf-8",

1
pack.bat Normal file
View File

@@ -0,0 +1 @@
python setup.py sdist bdist_wheel

View File

@@ -6,7 +6,7 @@ long_description = (here / "README.md").read_text(encoding="utf-8")
setup( setup(
name="logiliteal", name="logiliteal",
version="0.1.1", version="0.1.2",
description="简洁,高扩展性,可自定义的日志库 / Simple, high extensibility, and customizable logging library", description="简洁,高扩展性,可自定义的日志库 / Simple, high extensibility, and customizable logging library",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",

View File

@@ -11,94 +11,116 @@ from ..utils.fmt import fmt_file, fmt_message, fmt_console
from ..utils.configs import get_config, set_config from ..utils.configs import get_config, set_config
from ..utils.time import get_asctime from ..utils.time import get_asctime
import pathlib import pathlib
from pathlib import Path
def _get_full_path(file_path, file_name): def _get_full_path(file_path, file_name):
file_path.mkdir(parents=True, exist_ok=True) file_path.mkdir(parents=True, exist_ok=True)
return file_path / file_name return file_path / file_name
file_path = pathlib.Path(get_config("file_path"))
file_name = get_config("file_name")
file_format = get_config("file_format")
file_encoding = get_config("file_encoding")
is_enable_console = get_config("enable_console")
is_enable_file = get_config("enable_file")
class Logger: class Logger:
def __init__(self): def __init__(self):
if pathlib.Path(file_path).exists(): project_root = Path(__file__).parent.parent.parent.parent
if not pathlib.Path(file_path).is_dir(): file_path = project_root / get_config("file_path")
self.warn("日志文件路径不是目录,已自动自动使用默认目录") file_path.mkdir(parents=True, exist_ok=True)
if file_path.exists():
if not file_path.is_dir():
self.warn("日志文件路径不是目录,已自动使用默认目录")
set_config("file_path", "./logs") set_config("file_path", "./logs")
pathlib.Path("./logs").mkdir(parents=True, exist_ok=True) pathlib.Path("./logs").mkdir(parents=True, exist_ok=True)
if _get_full_path(file_path, file_name).exists(): current_file = _get_full_path(file_path, get_config("file_name"))
if current_file.exists():
from os import rename from os import rename
rename(_get_full_path(file_path, file_name), _get_full_path(file_path, f"{get_asctime().replace(':', '-')}.log")) rename(current_file, _get_full_path(file_path, f"{get_asctime().replace(':', '-')}.log"))
self.debug("日志文件已存在,已自动重命名") self.debug("日志文件已存在,已自动重命名")
def _log(self, msg, pf, lvn): def _log(self, msg, pf, lvn, no_file: bool = False, no_console: bool = False):
if is_enable_file: project_root = Path(__file__).parent.parent.parent.parent
file_path = project_root / get_config("file_path")
file_path.mkdir(parents=True, exist_ok=True)
file_name = get_config("file_name")
file_encoding = get_config("file_encoding")
is_enable_file = get_config("enable_file")
is_enable_console = get_config("enable_console")
if not no_file and is_enable_file:
try:
with open(_get_full_path(file_path, file_name), "a", encoding=file_encoding) as f: with open(_get_full_path(file_path, file_name), "a", encoding=file_encoding) as f:
f.write(fmt_file(lvn, fmt_message(msg, no_placeholder=True), pf)) f.write(fmt_file(lvn, fmt_message(msg, no_placeholder=True, no_process=True), pf))
if is_enable_console: except Exception as e:
print(fmt_console(lvn, fmt_message(msg, no_placeholder=True), pf)) self.error(f"日志写入失败: {str(e)}", no_file=True)
if not no_console and is_enable_console:
console_output = fmt_console(lvn, fmt_message(msg, no_placeholder=True), pf)
if console_output is not None:
print(console_output)
return fmt_console(lvn, fmt_message(msg, no_placeholder=True), pf) return fmt_console(lvn, fmt_message(msg, no_placeholder=True), pf)
def debug(self, message: Any, prefix: str | None = None, level: int = 0) -> Optional[str]: def debug(self, message: Any, prefix: str | None = None, level: int = 0, no_file: bool = False, no_console: bool = False) -> Optional[str]:
""" """
调试日志 调试日志
Debug log Debug log
:param message: 消息内容 Message content :param message: 消息内容 Message content
:param prefix: 前缀 Prefix :param prefix: 前缀 Prefix
:param level: 日志级别 Log level(0~9) :param level: 日志级别 Log level(0~9)
:param no_file: 不写入文件 Do not write to file
:param no_console: 不输出到控制台 Do not output to console
""" """
return self._log(message, prefix, level) return self._log(message, prefix, level, no_file, no_console)
def info(self, message: Any, prefix: str | None = None, level: int = 10) -> Optional[str]: def info(self, message: Any, prefix: str | None = None, level: int = 10, no_file: bool = False, no_console: bool = False) -> Optional[str]:
""" """
信息日志 信息日志
Info log Info log
:param message: 消息内容 Message content :param message: 消息内容 Message content
:param prefix: 前缀 Prefix :param prefix: 前缀 Prefix
:param level: 日志级别 Log level(10~19) :param level: 日志级别 Log level(10~19)
:param no_file: 不写入文件 Do not write to file
:param no_console: 不输出到控制台 Do not output to console
""" """
return self._log(message, prefix, level) return self._log(message, prefix, level, no_file, no_console)
def warn(self, message: Any, prefix: str | None = None, level: int = 20) -> Optional[str]: def warn(self, message: Any, prefix: str | None = None, level: int = 20, no_file: bool = False, no_console: bool = False) -> Optional[str]:
""" """
警告日志 警告日志
Warn log Warn log
:param message: 消息内容 Message content :param message: 消息内容 Message content
:param prefix: 前缀 Prefix :param prefix: 前缀 Prefix
:param level: 日志级别 Log level(20~29) :param level: 日志级别 Log level(20~29)
:param no_file: 不写入文件 Do not write to file
:param no_console: 不输出到控制台 Do not output to console
""" """
return self._log(message, prefix, level) return self._log(message, prefix, level, no_file, no_console)
def error(self, message: Any, prefix: str | None = None, level: int = 30) -> Optional[str]: def error(self, message: Any, prefix: str | None = None, level: int = 30, no_file: bool = False, no_console: bool = False) -> Optional[str]:
""" """
错误日志 错误日志
Error log Error log
:param message: 消息内容 Message content :param message: 消息内容 Message content
:param prefix: 前缀 Prefix :param prefix: 前缀 Prefix
:param level: 日志级别 Log level(30~39) :param level: 日志级别 Log level(30~39)
:param no_file: 不写入文件 Do not write to file
:param no_console: 不输出到控制台 Do not output to console
""" """
return self._log(message, prefix, level) return self._log(message, prefix, level, no_file, no_console)
def critical(self, message: Any, prefix: str | None = None, level: int = 40) -> Optional[str]: def critical(self, message: Any, prefix: str | None = None, level: int = 40, no_file: bool = False, no_console: bool = False) -> Optional[str]:
""" """
严重错误日志 严重错误日志
Critical error log Critical error log
:param message: 消息内容 Message content :param message: 消息内容 Message content
:param prefix: 前缀 Prefix :param prefix: 前缀 Prefix
:param level: 日志级别 Log level(40~49) :param level: 日志级别 Log level(40~49)
:param no_file: 不写入文件 Do not write to file
:param no_console: 不输出到控制台 Do not output to console
""" """
return self._log(message, prefix, level) return self._log(message, prefix, level, no_file, no_console)
def log(self, message: Any, prefix: str | None = None, level: int = 50) -> Optional[str]: def log(self, message: Any, prefix: str | None = None, level: int = 50, no_file: bool = False, no_console: bool = False) -> Optional[str]:
""" """
自定义日志 自定义日志
Custom log Custom log
:param message: 消息内容 Message content :param message: 消息内容 Message content
:param prefix: 前缀 Prefix :param prefix: 前缀 Prefix
:param level: 日志级别 Log level(50~59...) :param level: 日志级别 Log level(50~59...)
:param no_file: 不写入文件 Do not write to file
:param no_console: 不输出到控制台 Do not output to console
""" """
return self._log(message, prefix, level) return self._log(message, prefix, level, no_file, no_console)

View File

@@ -9,11 +9,43 @@ py-logiliteal's config settings, used to set py-logiliteal's global config
import json import json
from os import remove from os import remove
import shutil import shutil
import os
from pathlib import Path from pathlib import Path
from typing import Union, Optional, Tuple from typing import Union, Optional, Tuple
from logging import error from logging import error
DEFAULT_CONFIG_PATH = "logger_config.json" def get_config_path():
# 检查环境变量
env_config = os.getenv('LOGILITEAL_CONFIG')
if env_config and os.path.exists(env_config):
return env_config
# 检查当前工作目录
cwd_config = os.path.join(os.getcwd(), 'logger_config.json')
if os.path.exists(cwd_config):
return cwd_config
# 检查XDG配置目录
xdg_config_dir = os.getenv('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
xdg_config_path = os.path.join(xdg_config_dir, 'logiliteal', 'logger_config.json')
# 创建目录(如果不存在)
if not os.path.exists(os.path.dirname(xdg_config_path)):
try:
os.makedirs(os.path.dirname(xdg_config_path), exist_ok=True)
except Exception as e:
error(f"创建配置目录失败: {e}")
# 回退到项目根目录的配置文件
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
fallback_config = os.path.join(project_root, 'logger_config.json')
if os.path.exists(fallback_config):
return fallback_config
else:
return xdg_config_path
return xdg_config_path
DEFAULT_CONFIG_PATH = get_config_path()
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"file_level": "DEBUG", "file_level": "DEBUG",
"file_name": "latest.log", "file_name": "latest.log",
@@ -23,7 +55,7 @@ DEFAULT_CONFIG = {
"enable_console": True, "enable_console": True,
"enable_file": True, "enable_file": True,
"console_color": True, "console_color": True,
"console_level": "INFO", "console_level": "DEBUG",
"console_format": "{time} {levelname} | {prefix}{message}", "console_format": "{time} {levelname} | {prefix}{message}",
"console_prefix": "Auto", "console_prefix": "Auto",
"console_encoding": "utf-8", "console_encoding": "utf-8",
@@ -94,6 +126,8 @@ def get_config(select: str = None) -> Union[dict, str, bool, int, None]:
g_config_cache = json.load(f) g_config_cache = json.load(f)
g_config_mtime = current_mtime g_config_mtime = current_mtime
else: else:
# 确保目录存在
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, "w", encoding="utf-8") as f: with open(config_path, "w", encoding="utf-8") as f:
json.dump(DEFAULT_CONFIG, f, indent=4) json.dump(DEFAULT_CONFIG, f, indent=4)
g_config_cache = DEFAULT_CONFIG g_config_cache = DEFAULT_CONFIG

View File

@@ -10,9 +10,20 @@ from .configs import get_config
from typing import Any, Optional from typing import Any, Optional
from .time import get_asctime, get_time, get_weekday, get_date from .time import get_asctime, get_time, get_weekday, get_date
from .styles import set_color, set_bg_color from .styles import set_color, set_bg_color
import re from .regex import (
process_links,
process_markdown_formats,
process_html_styles,
process_special_tags,
process_color_formatting
)
from .placeholder import process_placeholder, SafeDict
time_color = get_config("time_color")
if get_config("time_color") is None:
time_color = "#28ffb6"
else:
time_color = get_config("time_color")
def fmt_level(level: str) -> int: def fmt_level(level: str) -> int:
""" """
@@ -58,29 +69,28 @@ def fmt_placeholder(message: Any, use_date_color: bool = True) -> str:
:param message: 消息内容 Message content :param message: 消息内容 Message content
:return: 格式化后的消息 Formatted message :return: 格式化后的消息 Formatted message
""" """
class SafeDict(dict):
def __missing__(self, key):
return f'{{{key}}}'
if not isinstance(message, str): if not isinstance(message, str):
message = str(message) message = str(message)
if use_date_color:
message = message.format_map(SafeDict(
asctime = set_color(get_asctime(),time_color),
time = set_color(get_time(),time_color),
weekday = set_color(get_weekday(),time_color),
date = set_color(get_date(),time_color)
))
else:
message = message.format_map(SafeDict(
asctime = get_asctime(),
time = get_time(),
weekday = get_weekday(),
date = get_date(),
))
return message
def fmt_message(message: Any, no_placeholder: bool = False, no_color: bool = False) -> str: context = {
"asctime": set_color(get_asctime(), time_color) if use_date_color else get_asctime(),
"time": set_color(get_time(), time_color) if use_date_color else get_time(),
"weekday": set_color(get_weekday(), time_color) if use_date_color else get_weekday(),
"date": set_color(get_date(), time_color) if use_date_color else get_date(),
}
return process_placeholder(message, context)
def fmt_message(
message: Any,
no_placeholder: bool = False,
no_style: bool = False,
no_process: bool = False,
no_tags: bool = False,
no_links: bool = False,
no_markdown: bool = False,
no_html: bool = False,
) -> str:
""" """
格式化消息内容 格式化消息内容
Format message content Format message content
@@ -88,43 +98,29 @@ def fmt_message(message: Any, no_placeholder: bool = False, no_color: bool = Fal
:return: 格式化后的消息 Formatted message :return: 格式化后的消息 Formatted message
""" """
def process_color_tags(msg: str) -> str: def process_color_tags(msg: str, no_process: bool = False) -> str:
from io import StringIO processed = process_color_formatting(
output = StringIO() process_special_tags(
stack = [] process_html_styles(
last_end = 0 process_markdown_formats(
pattern = re.compile(r'(<#([0-9a-fA-F]{6})>|</>)') process_links(msg, no_links),
current_color = None no_markdown
),
for match in pattern.finditer(msg): no_html
output.write(msg[last_end:match.start()]) ),
tag = match.group(1) no_tags
last_end = match.end() ),
no_process
if tag.startswith('<#'): )
stack.append(current_color) return processed
current_color = match.group(2)
else:
if stack:
current_color = stack.pop()
else:
output.write(tag)
output.write(msg[last_end:])
result = output.getvalue()
output.close()
if current_color:
result += ''.join(f'<#{color}>' for color in reversed(stack)) if stack else f'<#{current_color}>'
return result
if no_color:
processed_message = str(message) processed_message = str(message)
else: if not no_placeholder:
processed_message = process_color_tags(str(message)) processed_message = fmt_placeholder(processed_message)
if no_placeholder: if not no_style:
processed_message = process_color_tags(processed_message)
if no_process:
return message
return processed_message return processed_message
else:
return process_color_tags(fmt_placeholder(processed_message)) if not no_color else fmt_placeholder(processed_message)
def fmt_level_name(level_name: str) -> str: def fmt_level_name(level_name: str) -> str:
if get_config("console_color") != True: if get_config("console_color") != True:
@@ -151,7 +147,7 @@ def fmt_console(level: int, message: Any, prefix: str | None = None) -> Optional
:return: 格式化后的消息 Formatted message :return: 格式化后的消息 Formatted message
""" """
console_level = get_config("console_level") console_level = get_config("console_level")
if level != -1 and fmt_level(console_level) > level: if level != -1 and level < fmt_level(console_level):
return None return None
fmt = get_config("console_format") fmt = get_config("console_format")
prefix = prefix or "" prefix = prefix or ""
@@ -171,13 +167,14 @@ def fmt_file(level: int, message: Any, prefix: str | None = None) -> Optional[st
""" """
fl = get_config("file_level") fl = get_config("file_level")
fmt = get_config("file_format") fmt = get_config("file_format")
if fmt_level(fl) > level: if level != -1 and level < fmt_level(fl):
return None return None
if prefix is None: if prefix is None:
prefix = "" prefix = ""
fmt = fmt_placeholder(fmt, use_date_color=False) fmt = fmt_placeholder(fmt, use_date_color=False)
return f"{fmt.format( return f"{fmt.format(
levelname = fmt_level_number(level), levelname = fmt_level_number(level),
prefix = fmt_message(prefix, no_placeholder=True, no_color=True), prefix = fmt_message(prefix, no_placeholder=True, no_style=True),
message = fmt_message(message, no_placeholder=True, no_color=True) message = fmt_message(message, no_placeholder=True, no_style=True)
)}\n" )}\n"

View File

@@ -0,0 +1,62 @@
"""
占位符处理工具
"""
from typing import Dict, Any, Optional
import re
class SafeDict(Dict[str, Any]):
"""安全的字典类,用于处理缺失键的占位符替换"""
def __missing__(self, key: str) -> str:
return f"{{{key}}}"
def process_placeholder(text: str, context: Optional[Dict[str, Any]] = None) -> str:
"""处理文本中的占位符替换
Args:
text: 包含占位符的原始文本
context: 用于替换占位符的上下文字典
Returns:
替换后的文本
"""
if not context:
return text
# 处理简单占位符 {key}
safe_context = SafeDict(**context)
text = text.format_map(safe_context)
# 处理条件占位符 {{if condition}}content{{endif}}
def replace_condition(match):
condition = match.group(1).strip()
content = match.group(2).strip()
# 简单条件解析仅支持key存在性检查
if condition.startswith('!'):
key = condition[1:].strip()
return '' if key in context else content
return content if condition in context else ''
text = re.sub(r'\{\{if\s+(.*?)\}\}([\s\S]*?)\{\{endif\}\}', replace_condition, text, flags=re.IGNORECASE)
# 处理循环占位符 {{for item in list}}content{{endfor}}
def replace_loop(match):
items = match.group(1).strip().split(' in ')
if len(items) != 2:
return match.group(0)
item_name, list_name = items
content = match.group(2).strip()
if list_name not in context or not isinstance(context[list_name], list):
return ''
result = []
for item in context[list_name]:
item_context = {item_name: item}
result.append(process_placeholder(content, item_context))
return ''.join(result)
text = re.sub(r'\{\{for\s+(.*?)\}\}([\s\S]*?)\{\{endfor\}\}', replace_loop, text, flags=re.IGNORECASE)
return text

View File

@@ -0,0 +1,128 @@
"""
正则表达式处理工具
"""
import re
from collections import deque
from .styles import set_color, set_bg_color
def process_links(text: str, no_process: bool = False) -> str:
"""处理链接标签(HTML和Markdown格式)"""
if no_process:
return text
link_stack = deque()
placeholder_count = 0
placeholders = {}
def replace_link(m):
nonlocal placeholder_count
placeholder_count += 1
if len(m.groups()) == 2 and m.group(2) and not m.group(1).startswith('http'):
# Markdown链接 [text](url)
url = m.group(2).strip()
text = m.group(1)
else:
url = m.group(1)
text = m.group(2)
placeholder = f"__LINK_PLACEHOLDER_{placeholder_count}__"
placeholders[placeholder] = (url if url else "#", text)
link_stack.append(placeholder)
return placeholder
text = re.sub(r'<a\s+href="([^"]+)">(.*?)</a>', replace_link, text, flags=re.DOTALL)
text = re.sub(r'<link\s+href="([^"]+)">(.*?)</link>', replace_link, text, flags=re.DOTALL)
text = re.sub(r'\[(.*?)\]\((.*?)\)', replace_link, text)
for placeholder, (url, text_content) in placeholders.items():
ansi_link = f'\033]8;;{url}\033\\{set_color("\033[4m" + text_content, "#5f93ff")}\033]8;;\033\\'
text = text.replace(placeholder, ansi_link)
return text
def process_markdown_formats(text: str, no_process: bool = False) -> str:
"""处理Markdown格式"""
if no_process:
return text
# Markdown粗体 (**text**)
text = re.sub(r'\*\*(.*?)\*\*', '\033[1m\\g<1>\033[22m', text)
# Markdown斜体 (*text*)
text = re.sub(r'\*(.*?)\*', '\033[3m\\g<1>\033[23m', text)
# Markdown下划线 (__text__)
text = re.sub(r'__(.*?)__', '\033[4m\\g<1>\033[24m', text)
# Markdown删除线 (~~text~~)
text = re.sub(r'~~(.*?)~~', '\033[9m\\g<1>\033[29m', text)
return text
def process_html_styles(text: str, no_process: bool = False) -> str:
"""处理HTML样式标签"""
if no_process:
return text
# HTML斜体 <i></i>
text = re.sub(r'<i>([^<]*?)(</i>|$)',
lambda m: '\033[3m' + m.group(1) + '\033[23m', text, flags=re.DOTALL)
# HTML粗体 <b></b>
text = re.sub(r'<b>([^<]*?)</b>',
lambda m: '\033[1m' + m.group(1) + '\033[22m', text)
# HTML下划线 <u></u>
text = re.sub(r'<u>([^<]*?)</u>',
lambda m: '\033[4m' + m.group(1) + '\033[24m', text)
# HTML删除线 <s></s>
text = re.sub(r'<s>([^<]*?)(</s>|$)',
lambda m: '\033[9m' + m.group(1) + '\033[29m', text, flags=re.DOTALL)
return text
def process_special_tags(text: str, no_process: bool = False) -> str:
"""处理特殊标签(换行、重置、段落)"""
if no_process:
return text
text = re.sub(r'<br>', '\n', text)
text = re.sub(r'<c>', '\033[0m', text)
# 处理段落标签
text = re.sub(r'<p>(.*?)</p>', r'\n\033[0m\\g<1>\033[0m\n', text, flags=re.DOTALL)
text = re.sub(r'<p>(.*?)(</p>|$)', r'\n\033[0m\\g<1>\033[0m\n', text, flags=re.DOTALL)
text = re.sub(r'</p>', '\033[0m\n', text)
return text
def process_color_formatting(text: str, no_process: bool = False) -> str:
"""处理颜色标签"""
if no_process:
return text
color_pattern = r'<#([0-9a-fA-F]{6})>'
close_pattern = r'</>'
parts = re.split(f'({color_pattern}|{close_pattern})', text)
result = []
color_stack = []
for part in parts:
if part and re.fullmatch(color_pattern, part):
color = re.match(color_pattern, part).group(1)
color_stack.append(color)
continue
elif part == '</>':
if color_stack:
color_stack.pop()
continue
elif part:
if color_stack:
current_color = color_stack[-1]
r = int(current_color[0:2], 16)
g = int(current_color[2:4], 16)
b = int(current_color[4:6], 16)
ansi_code = f'\033[38;2;{r};{g};{b}m'
reset_code = '\033[0m'
result.append(f'{ansi_code}{part}{reset_code}')
else:
processed_text = part
processed_text = re.sub(r'<#([0-9a-fA-F]{6})>', lambda m: f'<{set_color(f"#{m.group(1)}")}>', processed_text)
result.append(processed_text)
processed_text = ''.join(result)
processed_text = re.sub(f'{color_pattern}|{close_pattern}', '', processed_text)
processed_text = re.sub(r'[0-9a-fA-F]{6}', '', processed_text)
return processed_text

View File

@@ -57,13 +57,15 @@ def set_bg_color(text: str|None, color: str|None) -> str:
ansi = ansi.replace("38;", "48;") ansi = ansi.replace("38;", "48;")
return f"{ansi}{text}\033[0m" return f"{ansi}{text}\033[0m"
def set_style(text: str|None, bold: bool = False, underline: bool = False, reverse: bool = False) -> str: def set_style(text: str|None, bold: bool = False, italic: bool = False, underline: bool = False, strikethrough: bool = False, reverse: bool = False) -> str:
""" """
设置文本样式 设置文本样式
Set text style Set text style
:param text: 文本内容 Text content :param text: 文本内容 Text content
:param bold: 是否加粗 Is bold :param bold: 是否加粗 Is bold
:param italic: 是否斜体 Is italic
:param underline: 是否下划线 Is underline :param underline: 是否下划线 Is underline
:param strikethrough: 是否划线 Is strikethrough
:param reverse: 是否反相 Is reverse :param reverse: 是否反相 Is reverse
:return: 格式化后的文本 Formatted text :return: 格式化后的文本 Formatted text
""" """
@@ -72,8 +74,12 @@ def set_style(text: str|None, bold: bool = False, underline: bool = False, rever
ansi = "" ansi = ""
if bold: if bold:
ansi += "\033[1m" ansi += "\033[1m"
if italic:
ansi += "\033[3m"
if underline: if underline:
ansi += "\033[4m" ansi += "\033[4m"
if strikethrough:
ansi += "\033[9m"
if reverse: if reverse:
ansi += "\033[7m" ansi += "\033[7m"
return f"{ansi}{text}\033[0m" return f"{ansi}{text}\033[0m"

View File

@@ -9,12 +9,6 @@ Time utility module, used for time formatting and output, caching time formattin
from datetime import datetime from datetime import datetime
from .configs import get_config from .configs import get_config
import time
cache_time: str = ""
cache_time_ts: float = 0.0
cache_fmt: str | None = None
def get_asctime() -> str: def get_asctime() -> str:
""" """
获取当前时间(YYYY-MM-DD HH:MM:SS),并缓存格式化结果 获取当前时间(YYYY-MM-DD HH:MM:SS),并缓存格式化结果
@@ -54,12 +48,5 @@ def _get_time(fmt: str) -> str:
:param fmt: 时间格式 Time format :param fmt: 时间格式 Time format
:return: 格式化后的时间 Formatted time :return: 格式化后的时间 Formatted time
""" """
global cache_time, cache_time_ts, cache_fmt
if cache_fmt is None:
cache_fmt = fmt
now = time.time()
if cache_time and (now - cache_time_ts < 1) and (cache_fmt == fmt):
return cache_time
cache_time = datetime.now().strftime(fmt) cache_time = datetime.now().strftime(fmt)
cache_time_ts = now
return cache_time return cache_time

View File

@@ -1,32 +0,0 @@
{
"file_level": "DEBUG",
"file_name": "latest.log",
"file_path": "./logs",
"file_format": "{asctime} {levelname} | {prefix}{message}",
"file_encoding": "utf-8",
"enable_console": true,
"enable_file": true,
"console_color": true,
"console_level": "INFO",
"console_format": "{time} {levelname} | {prefix}{message}",
"console_prefix": "Auto",
"console_encoding": "utf-8",
"asctime_format": "%Y-%m-%d %H:%M:%S",
"time_format": "%H:%M:%S",
"date_format": "%Y-%m-%d",
"weekday_format": "%A",
"level_name": {
"DEBUG": "DEBUG",
"INFO": "INFO",
"WARN": "WARN",
"ERRO": "ERRO",
"CRIT": "CRIT"
},
"level_color": {
"DEBUG": "#c1d5ff",
"INFO": "#c1ffff",
"WARN": "#fff600",
"ERRO": "#ffa000",
"CRIT": "#ff8181"
}
}

53
tests/t-color-priority.py Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试颜色优先级处理功能
"""
import sys
import os
from pathlib import Path
project_root = Path(__file__).parent.parent
sys.path.append(str(project_root))
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from src.logiliteal.utils.fmt import fmt_message
def test_color_priority():
"""测试颜色优先级"""
print("=== 颜色优先级测试 ===")
# 测试1: 基本颜色标签
test1 = "<#ff0000>红色文本</>"
print(f"基本颜色: {test1}")
print(f"结果: {fmt_message(test1)}")
print()
# 测试2: 嵌套颜色(内层优先级高)
test2 = "<#ff0000>外层<#00ff00>内层</>外层</>"
print(f"嵌套颜色: {test2}")
print(f"结果: {fmt_message(test2)}")
print()
# 测试3: 颜色重叠测试(最后边颜色为主)
test3 = "<#ff0000>颜色重叠<#0000ff>测<#ff00c2>试</></></>"
print(f"颜色重叠: {test3}")
print(f"结果: {fmt_message(test3)}")
print()
# 测试4: 多层嵌套
test4 = "<#ff0000>1<#00ff00>2<#0000ff>3<#ffff00>4</>3</>2</>1</>"
print(f"多层嵌套: {test4}")
print(f"结果: {fmt_message(test4)}")
print()
# 测试5: 未闭合标签(忽略)
test5 = "<#ff0000>未闭合标签"
print(f"未闭合标签: {test5}")
print(f"结果: {fmt_message(test5)}")
print()
if __name__ == "__main__":
test_color_priority()

34
tests/t-fmt-text.py Normal file
View File

@@ -0,0 +1,34 @@
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent
sys.path.append(str(project_root))
from src.logiliteal.levels import Logger
log = Logger()
log.info("<i>html斜体</i>")
log.info("<b>html加粗</b>")
log.info("<u>html下划线</u>")
log.info("<s>html删除线</s>")
log.info("<p>html段落</p>")
log.info("<a href=\"https://www.baidu.com\">html超链接</a>")
log.info("**Markdown加粗**")
log.info("*Markdown斜体*")
log.info("`Markdown代码块`")
log.info("~~Markdown删除线~~\n")
log.info("[md超链接](https://www.baidu.com)")
log.info("--测试<i>重复--")
log.info("<#ff0000>颜色重叠<#0000ff>测<#ff00c2>试</></></>")
log.info("<a href=\"https://www.baidu.com\">超<link href=\"https://www.bing.cn\">链接[重叠](https://www.360.com)</link></a>")
"""
while True:
try:
log.info(get_asctime())
time.sleep(1)
except KeyboardInterrupt:
log.info("测试结束")
break
"""

View File

@@ -8,8 +8,8 @@ from src.logiliteal import Logger
log = Logger() log = Logger()
log.info("测试信息日志")
log.debug("测试调试日志") log.debug("测试调试日志")
log.info("测试信息日志")
log.warn("测试警告日志") log.warn("测试警告日志")
log.error("测试错误日志") log.error("测试错误日志")
log.critical("测试严重错误日志") log.critical("测试严重错误日志")

View File

@@ -1,25 +0,0 @@
import sys
from pathlib import Path
import time
project_root = Path(__file__).parent.parent
sys.path.append(str(project_root))
from src.logiliteal.utils.time import get_asctime, get_time, get_date, get_weekday
from src.logiliteal.levels import Logger
log = Logger()
log.info(get_asctime())
log.info(get_time())
log.info(get_date())
log.info(get_weekday())
while True:
try:
log.info(get_asctime())
time.sleep(1)
log.info("时间分割线")
except KeyboardInterrupt:
log.info("测试结束")
break