mirror of
https://github.com/Nanaloveyuki/py-logiliteal.git
synced 2025-10-19 16:46:23 +00:00
Compare commits
7 Commits
b0.1.1
...
80e4a4d02f
Author | SHA1 | Date | |
---|---|---|---|
|
80e4a4d02f | ||
|
eed6144970 | ||
|
796a8a82f6 | ||
|
89bc381235 | ||
|
89cb26b9b3 | ||
|
9b5d7cdfad | ||
|
d5f157bdf3 |
30
README.md
30
README.md
@@ -14,8 +14,38 @@ pip install logiliteal
|
||||
**支持高可扩展的样式**
|
||||
- 支持使用HEX十六进制颜色代码`<#ffffff>text</>`渲染颜色
|
||||
- 支持使用占位符`{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 3.13.5
|
||||
- Python 3.13.4
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"enable_console": true,
|
||||
"enable_file": true,
|
||||
"console_color": true,
|
||||
"console_level": "INFO",
|
||||
"console_level": "DEBUG",
|
||||
"console_format": "{time} {levelname} | {prefix}{message}",
|
||||
"console_prefix": "Auto",
|
||||
"console_encoding": "utf-8",
|
||||
|
2
setup.py
2
setup.py
@@ -6,7 +6,7 @@ long_description = (here / "README.md").read_text(encoding="utf-8")
|
||||
|
||||
setup(
|
||||
name="logiliteal",
|
||||
version="0.1.1",
|
||||
version="0.1.2",
|
||||
description="简洁,高扩展性,可自定义的日志库 / Simple, high extensibility, and customizable logging library",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
@@ -11,94 +11,116 @@ from ..utils.fmt import fmt_file, fmt_message, fmt_console
|
||||
from ..utils.configs import get_config, set_config
|
||||
from ..utils.time import get_asctime
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
|
||||
def _get_full_path(file_path, file_name):
|
||||
file_path.mkdir(parents=True, exist_ok=True)
|
||||
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:
|
||||
def __init__(self):
|
||||
if pathlib.Path(file_path).exists():
|
||||
if not pathlib.Path(file_path).is_dir():
|
||||
self.warn("日志文件路径不是目录,已自动自动使用默认目录")
|
||||
project_root = Path(__file__).parent.parent.parent.parent
|
||||
file_path = project_root / get_config("file_path")
|
||||
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")
|
||||
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
|
||||
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("日志文件已存在,已自动重命名")
|
||||
|
||||
def _log(self, msg, pf, lvn):
|
||||
if is_enable_file:
|
||||
def _log(self, msg, pf, lvn, no_file: bool = False, no_console: bool = False):
|
||||
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:
|
||||
f.write(fmt_file(lvn, fmt_message(msg, no_placeholder=True), pf))
|
||||
if is_enable_console:
|
||||
print(fmt_console(lvn, fmt_message(msg, no_placeholder=True), pf))
|
||||
f.write(fmt_file(lvn, fmt_message(msg, no_placeholder=True, no_process=True), pf))
|
||||
except Exception as e:
|
||||
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)
|
||||
|
||||
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
|
||||
:param message: 消息内容 Message content
|
||||
:param prefix: 前缀 Prefix
|
||||
: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
|
||||
:param message: 消息内容 Message content
|
||||
:param prefix: 前缀 Prefix
|
||||
: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
|
||||
:param message: 消息内容 Message content
|
||||
:param prefix: 前缀 Prefix
|
||||
: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
|
||||
:param message: 消息内容 Message content
|
||||
:param prefix: 前缀 Prefix
|
||||
: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
|
||||
:param message: 消息内容 Message content
|
||||
:param prefix: 前缀 Prefix
|
||||
: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
|
||||
:param message: 消息内容 Message content
|
||||
:param prefix: 前缀 Prefix
|
||||
: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)
|
||||
|
@@ -9,11 +9,43 @@ py-logiliteal's config settings, used to set py-logiliteal's global config
|
||||
import json
|
||||
from os import remove
|
||||
import shutil
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Union, Optional, Tuple
|
||||
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 = {
|
||||
"file_level": "DEBUG",
|
||||
"file_name": "latest.log",
|
||||
@@ -23,7 +55,7 @@ DEFAULT_CONFIG = {
|
||||
"enable_console": True,
|
||||
"enable_file": True,
|
||||
"console_color": True,
|
||||
"console_level": "INFO",
|
||||
"console_level": "DEBUG",
|
||||
"console_format": "{time} {levelname} | {prefix}{message}",
|
||||
"console_prefix": "Auto",
|
||||
"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_mtime = current_mtime
|
||||
else:
|
||||
# 确保目录存在
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(DEFAULT_CONFIG, f, indent=4)
|
||||
g_config_cache = DEFAULT_CONFIG
|
||||
|
@@ -10,8 +10,19 @@ from .configs import get_config
|
||||
from typing import Any, Optional
|
||||
from .time import get_asctime, get_time, get_weekday, get_date
|
||||
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
|
||||
|
||||
|
||||
if get_config("time_color") is None:
|
||||
time_color = "#28ffb6"
|
||||
else:
|
||||
time_color = get_config("time_color")
|
||||
|
||||
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
|
||||
:return: 格式化后的消息 Formatted message
|
||||
"""
|
||||
class SafeDict(dict):
|
||||
def __missing__(self, key):
|
||||
return f'{{{key}}}'
|
||||
|
||||
if not isinstance(message, str):
|
||||
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
|
||||
@@ -88,43 +98,29 @@ def fmt_message(message: Any, no_placeholder: bool = False, no_color: bool = Fal
|
||||
:return: 格式化后的消息 Formatted message
|
||||
"""
|
||||
|
||||
def process_color_tags(msg: str) -> str:
|
||||
from io import StringIO
|
||||
output = StringIO()
|
||||
stack = []
|
||||
last_end = 0
|
||||
pattern = re.compile(r'(<#([0-9a-fA-F]{6})>|</>)')
|
||||
current_color = None
|
||||
|
||||
for match in pattern.finditer(msg):
|
||||
output.write(msg[last_end:match.start()])
|
||||
tag = match.group(1)
|
||||
last_end = match.end()
|
||||
|
||||
if tag.startswith('<#'):
|
||||
stack.append(current_color)
|
||||
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:
|
||||
def process_color_tags(msg: str, no_process: bool = False) -> str:
|
||||
processed = process_color_formatting(
|
||||
process_special_tags(
|
||||
process_html_styles(
|
||||
process_markdown_formats(
|
||||
process_links(msg, no_links),
|
||||
no_markdown
|
||||
),
|
||||
no_html
|
||||
),
|
||||
no_tags
|
||||
),
|
||||
no_process
|
||||
)
|
||||
return processed
|
||||
processed_message = str(message)
|
||||
else:
|
||||
processed_message = process_color_tags(str(message))
|
||||
if no_placeholder:
|
||||
if not no_placeholder:
|
||||
processed_message = fmt_placeholder(processed_message)
|
||||
if not no_style:
|
||||
processed_message = process_color_tags(processed_message)
|
||||
if no_process:
|
||||
return 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:
|
||||
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
|
||||
"""
|
||||
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
|
||||
fmt = get_config("console_format")
|
||||
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")
|
||||
fmt = get_config("file_format")
|
||||
if fmt_level(fl) > level:
|
||||
if level != -1 and level < fmt_level(fl):
|
||||
return None
|
||||
if prefix is None:
|
||||
prefix = ""
|
||||
fmt = fmt_placeholder(fmt, use_date_color=False)
|
||||
return f"{fmt.format(
|
||||
levelname = fmt_level_number(level),
|
||||
prefix = fmt_message(prefix, no_placeholder=True, no_color=True),
|
||||
message = fmt_message(message, no_placeholder=True, no_color=True)
|
||||
prefix = fmt_message(prefix, no_placeholder=True, no_style=True),
|
||||
message = fmt_message(message, no_placeholder=True, no_style=True)
|
||||
)}\n"
|
||||
|
||||
|
62
src/logiliteal/utils/placeholder.py
Normal file
62
src/logiliteal/utils/placeholder.py
Normal 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
|
128
src/logiliteal/utils/regex.py
Normal file
128
src/logiliteal/utils/regex.py
Normal 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
|
@@ -57,13 +57,15 @@ def set_bg_color(text: str|None, color: str|None) -> str:
|
||||
ansi = ansi.replace("38;", "48;")
|
||||
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
|
||||
:param text: 文本内容 Text content
|
||||
:param bold: 是否加粗 Is bold
|
||||
:param italic: 是否斜体 Is italic
|
||||
:param underline: 是否下划线 Is underline
|
||||
:param strikethrough: 是否划线 Is strikethrough
|
||||
:param reverse: 是否反相 Is reverse
|
||||
:return: 格式化后的文本 Formatted text
|
||||
"""
|
||||
@@ -72,8 +74,12 @@ def set_style(text: str|None, bold: bool = False, underline: bool = False, rever
|
||||
ansi = ""
|
||||
if bold:
|
||||
ansi += "\033[1m"
|
||||
if italic:
|
||||
ansi += "\033[3m"
|
||||
if underline:
|
||||
ansi += "\033[4m"
|
||||
if strikethrough:
|
||||
ansi += "\033[9m"
|
||||
if reverse:
|
||||
ansi += "\033[7m"
|
||||
return f"{ansi}{text}\033[0m"
|
||||
|
@@ -9,12 +9,6 @@ Time utility module, used for time formatting and output, caching time formattin
|
||||
from datetime import datetime
|
||||
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:
|
||||
"""
|
||||
获取当前时间(YYYY-MM-DD HH:MM:SS),并缓存格式化结果
|
||||
@@ -54,12 +48,5 @@ def _get_time(fmt: str) -> str:
|
||||
:param fmt: 时间格式 Time format
|
||||
: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_ts = now
|
||||
return cache_time
|
@@ -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
53
tests/t-color-priority.py
Normal 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
34
tests/t-fmt-text.py
Normal 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
|
||||
"""
|
@@ -8,8 +8,8 @@ from src.logiliteal import Logger
|
||||
|
||||
log = Logger()
|
||||
|
||||
log.info("测试信息日志")
|
||||
log.debug("测试调试日志")
|
||||
log.info("测试信息日志")
|
||||
log.warn("测试警告日志")
|
||||
log.error("测试错误日志")
|
||||
log.critical("测试严重错误日志")
|
@@ -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
|
Reference in New Issue
Block a user