ForcePilot/backend/package/yuxi/channels/adapters/imessage/formatter.py

125 lines
3.7 KiB
Python
Raw Normal View History

from __future__ import annotations
import re
TABLE_ROW_RE = re.compile(r"^\|(.+)\|$")
TABLE_SEP_RE = re.compile(r"^\|[\s\-:\|]+\|$")
DIRECTIVE_RE = re.compile(r"<\s*/?directive[^>]*>", re.IGNORECASE)
def convert_markdown_tables(text: str) -> str:
"""将 Markdown 表格转换为可读的 bullets 格式。
iMessage 不支持 Markdown 表格渲染此函数将表格行转为可读的列表
"""
lines = text.split("\n")
result: list[str] = []
i = 0
while i < len(lines):
line = lines[i]
if i + 2 < len(lines) and TABLE_ROW_RE.match(line) and TABLE_SEP_RE.match(lines[i + 1]):
header = line
sep = lines[i + 1]
body_rows: list[str] = []
j = i + 2
while j < len(lines) and TABLE_ROW_RE.match(lines[j]):
body_rows.append(lines[j])
j += 1
converted = _table_to_bullets(header, sep, body_rows)
result.extend(converted)
i = j
continue
result.append(line)
i += 1
return "\n".join(result)
def _table_to_bullets(header: str, sep: str, body_rows: list[str]) -> list[str]:
header_cells = _parse_cells(header)
sep_cells = _parse_cells(sep)
if not header_cells:
return [header, sep] + body_rows
alignments = _detect_alignments(sep_cells, len(header_cells))
body_cells = [_parse_cells(row) for row in body_rows]
output: list[str] = []
for row_idx, cells in enumerate([header_cells] + body_cells):
if row_idx == 0:
output.append(f"{', '.join(cells)}")
elif len(cells) <= 2:
output.append(" " + ": ".join(cells))
else:
parts: list[str] = []
for ci, (cell, hdr, align) in enumerate(zip(cells, header_cells, alignments)):
prefix = f" {hdr}: " if ci == 0 or align != "skip" else f" {hdr}: "
parts.append(f"{prefix}{cell}")
output.append("")
output.extend(parts)
return output
def _parse_cells(row: str) -> list[str]:
return [cell.strip() for cell in row.strip("|").split("|")]
def _detect_alignments(sep_cells: list[str], col_count: int) -> list[str]:
aligns: list[str] = []
for i in range(col_count):
if i < len(sep_cells):
cell = sep_cells[i]
left = cell.startswith(":")
right = cell.endswith(":")
if left and right:
aligns.append("center")
elif right:
aligns.append("right")
else:
aligns.append("left")
else:
aligns.append("left")
return aligns
def sanitize_imessage_text(text: str) -> str:
"""净化出站文本,移除 iMessage 中不支持的格式。
- 去除 \\r 字符换行标准化
- 去除 <directive> 内联指令标签
- 去除长度前缀损坏 \"123\\nThis is message\"
- 过滤控制字符
- 合并多余空白行
"""
text = text.replace("\r", "")
text = DIRECTIVE_RE.sub("", text)
text = _strip_length_prefix(text)
text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]", "", text)
text = re.sub(r"\n{3,}", "\n\n", text)
text = text.strip()
return text
def _strip_length_prefix(text: str) -> str:
"""去除 iMessage 长度前缀损坏格式。
BlueBubbles 可能在某些消息前附加长度前缀如 \"42\\nHello world\"
去除数字换行前缀
"""
match = re.match(r"^(\d+)\n", text)
if match:
prefix_len = int(match.group(1))
remaining = text[match.end() :]
if abs(len(remaining) - prefix_len) <= 5:
return remaining
return text