From b867004f00e42f296d153fa7bd2fe38a7c7b5208 Mon Sep 17 00:00:00 2001 From: GitHub FrozenYears <1939648535@qq.com> Date: Wed, 25 Mar 2026 18:33:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=9B=BE=E5=83=8F?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=20HTML=20=E5=92=8C=20Base64=20=E4=B9=B1?= =?UTF-8?q?=E7=A0=81=E5=8F=8A=E6=B8=B2=E6=9F=93=E5=9B=BE=E7=89=87=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E8=B6=85=E9=95=BF=E6=96=87=E6=9C=AC=E6=92=91=E7=88=86?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=AF=BC=E8=87=B4=E6=B6=88=E6=81=AF=E5=8F=91?= =?UTF-8?q?=E9=80=81=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _conf_schema.json | 6 - main.py | 10 +- .../services/message_cleaner_service.py | 36 ++++++ .../analyzers/chat_quality_analyzer.py | 5 +- .../analyzers/golden_quote_analyzer.py | 7 +- .../analysis/analyzers/topic_analyzer.py | 4 +- .../analysis/analyzers/user_title_analyzer.py | 12 +- .../analysis/utils/json_utils.py | 114 +++++++++++------- src/infrastructure/config/config_manager.py | 9 -- .../templates/format/image_template.html | 35 ++++++ .../templates/format/quote_item.html | 2 +- .../templates/hack/image_template.html | 45 +++++++ .../reporting/templates/hack/quote_item.html | 2 +- .../retro_futurism/image_template.html | 36 ++++++ .../templates/retro_futurism/quote_item.html | 2 +- .../templates/scrapbook/image_template.html | 21 ++++ .../templates/simple/image_template.html | 6 + .../templates/simple/quote_item.html | 2 +- .../spring_festival/image_template.html | 24 ++++ .../templates/spring_festival/quote_item.html | 2 +- 20 files changed, 305 insertions(+), 75 deletions(-) diff --git a/_conf_schema.json b/_conf_schema.json index ae5d059..1aa564c 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -88,12 +88,6 @@ "description": "图片传输使用 Base64 编码", "default": true, "hint": "启用后图片会被转为 Base64 编码传输。关闭后将直接传递文件路径或 URL 给 OneBot 实现端,适用于 go-cqhttp、Lagrange 等支持本地路径的实现,可降低内存占用。" - }, - "enable_analysis_reply": { - "type": "bool", - "description": "发送文本/表情回复", - "default": false, - "hint": "开启后,/群分析 将通过发送文本消息提示进度;关闭(默认)则使用表情回应。" } } }, diff --git a/main.py b/main.py index 74a14df..9573656 100644 --- a/main.py +++ b/main.py @@ -504,14 +504,10 @@ async def analyze_group_daily( ) TraceContext.set(trace_id) - # 表情回应 或 文本提示(二选一,由配置开关控制) + # 使用表情回应代替文本回复 adapter = self.bot_manager.get_adapter(platform_id) orig_msg_id = getattr(event.message_obj, "message_id", None) - use_text_reply = self.config_manager.get_enable_analysis_reply() - - if use_text_reply: - yield event.plain_result("🔍 正在启动分析引擎,正在拉取最近消息...") - elif adapter and orig_msg_id: + if adapter and orig_msg_id: await adapter.set_reaction(event.get_group_id(), orig_msg_id, "🔍") # 🔍 try: @@ -528,7 +524,7 @@ async def analyze_group_daily( yield event.plain_result("❌ 分析失败,原因未知") return - if not use_text_reply and adapter and orig_msg_id: + if adapter and orig_msg_id: await adapter.set_reaction( event.get_group_id(), orig_msg_id, "📊" ) # 📊 diff --git a/src/domain/services/message_cleaner_service.py b/src/domain/services/message_cleaner_service.py index 0d4f59f..371af2e 100644 --- a/src/domain/services/message_cleaner_service.py +++ b/src/domain/services/message_cleaner_service.py @@ -107,3 +107,39 @@ def clean_messages( cleaned_list.append(new_msg) return cleaned_list + + @staticmethod + def sanitize_chat_text(text: str) -> str: + """ + Remove HTML tags, Base64 data URIs, and control characters from chat text + before LLM processing. + + This prevents contaminated data from breaking JSON parsing in LLM responses + and causing layout issues in rendered HTML reports. + + Args: + text: Raw chat message text + + Returns: + Cleaned text with only plain content + """ + if not text: + return "" + + # Remove Base64 data URIs first (longer pattern, should be removed before tags) + text = re.sub(r"data:[^;]+;base64,[A-Za-z0-9+/=]+", "", text) + + # Remove HTML tags (including self-closing and attributes) + text = re.sub(r"<[^>]+>", "", text) + + # Remove control characters (ASCII 0x00-0x1F, 0x7F-0x9F) + text = re.sub(r"[\x00-\x1f\x7f-\x9f]", "", text) + + # Normalize Unicode quotes to ASCII equivalents + text = text.replace("\u201c", '"').replace("\u201d", '"') # " " + text = text.replace("\u2018", "'").replace("\u2019", "'") # ' ' + + # Clean up extra whitespace from removals + text = re.sub(r"\s+", " ", text).strip() + + return text diff --git a/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py b/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py index 43ad8e3..d6c4451 100644 --- a/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py +++ b/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py @@ -6,6 +6,7 @@ from datetime import datetime from ....domain.models.data_models import QualityDimension, QualityReview, TokenUsage +from ....domain.services.message_cleaner_service import MessageCleanerService from ....utils.logger import logger from ..utils import InfoUtils from ..utils.json_utils import extract_quality_with_regex, parse_json_object_response @@ -79,7 +80,9 @@ def build_prompt(self, data: list[dict]) -> str: combined_text = "".join(text_parts).strip() if combined_text and not combined_text.startswith("/"): - text_messages.append(f"[{msg_time}] [{nickname}]: {combined_text}") + sanitized_text = MessageCleanerService.sanitize_chat_text(combined_text) + if sanitized_text: + text_messages.append(f"[{msg_time}] [{nickname}]: {sanitized_text}") messages_text = "\n".join(text_messages[:1000]) diff --git a/src/infrastructure/analysis/analyzers/golden_quote_analyzer.py b/src/infrastructure/analysis/analyzers/golden_quote_analyzer.py index 06be48c..ef5662c 100644 --- a/src/infrastructure/analysis/analyzers/golden_quote_analyzer.py +++ b/src/infrastructure/analysis/analyzers/golden_quote_analyzer.py @@ -6,6 +6,7 @@ from datetime import datetime from ....domain.models.data_models import GoldenQuote, TokenUsage +from ....domain.services.message_cleaner_service import MessageCleanerService from ....utils.logger import logger from ..utils import InfoUtils from ..utils.json_utils import extract_golden_quotes_with_regex @@ -54,8 +55,12 @@ def build_prompt(self, data: list[dict]) -> str: return "" # 构建消息文本 (用 [user_id] 替代 nickname 以确保回填 100% 准确,避免 Emoji 等干扰) + # 应用 sanitize_chat_text 清洗 HTML 标签和 Base64 数据 messages_text = "\n".join( - [f"[{msg['time']}] [{msg['user_id']}]: {msg['content']}" for msg in data] + [ + f"[{msg['time']}] [{msg['user_id']}]: {MessageCleanerService.sanitize_chat_text(msg['content'])}" + for msg in data + ] ) max_golden_quotes = self.get_max_count() diff --git a/src/infrastructure/analysis/analyzers/topic_analyzer.py b/src/infrastructure/analysis/analyzers/topic_analyzer.py index a9f9958..9b2318a 100644 --- a/src/infrastructure/analysis/analyzers/topic_analyzer.py +++ b/src/infrastructure/analysis/analyzers/topic_analyzer.py @@ -7,6 +7,7 @@ from datetime import datetime from ....domain.models.data_models import SummaryTopic, TokenUsage +from ....domain.services.message_cleaner_service import MessageCleanerService from ....utils.logger import logger from ..utils import InfoUtils from ..utils.json_utils import extract_topics_with_regex @@ -149,9 +150,10 @@ def build_prompt(self, data: list[dict]) -> str: # 构建消息文本 # 使用用户提供的 ID-Only 格式: [HH:MM] [用户ID]: 消息内容 + # 应用 sanitize_chat_text 清洗 HTML 标签和 Base64 数据 messages_text = "\n".join( [ - f"[{msg['time']}] [{msg['user_id']}]: {msg['content']}" + f"[{msg['time']}] [{msg['user_id']}]: {MessageCleanerService.sanitize_chat_text(msg['content'])}" for msg in text_messages ] ) diff --git a/src/infrastructure/analysis/analyzers/user_title_analyzer.py b/src/infrastructure/analysis/analyzers/user_title_analyzer.py index 107d695..daac889 100644 --- a/src/infrastructure/analysis/analyzers/user_title_analyzer.py +++ b/src/infrastructure/analysis/analyzers/user_title_analyzer.py @@ -4,6 +4,7 @@ """ from ....domain.models.data_models import TokenUsage, UserTitle +from ....domain.services.message_cleaner_service import MessageCleanerService from ....utils.logger import logger from ..utils.json_utils import extract_user_titles_with_regex from .base_analyzer import BaseAnalyzer @@ -51,12 +52,15 @@ def build_prompt(self, data: dict) -> str: return "" # 构建用户数据文本 + # 应用 sanitize_chat_text 清洗 HTML 标签和 Base64 数据 users_text = "\n".join( [ - f"- {user['name']} (ID:{user['user_id']}): " - f"发言{user['message_count']}条, 平均{user['avg_chars']}字, " - f"表情比例{user['emoji_ratio']}, 夜间发言比例{user['night_ratio']}, " - f"回复比例{user['reply_ratio']}" + ( + f"- {MessageCleanerService.sanitize_chat_text(user['name'])} (ID:{user['user_id']}): " + f"发言{user['message_count']}条, 平均{user['avg_chars']}字, " + f"表情比例{user['emoji_ratio']}, 夜间发言比例{user['night_ratio']}, " + f"回复比例{user['reply_ratio']}" + ) for user in user_summaries ] ) diff --git a/src/infrastructure/analysis/utils/json_utils.py b/src/infrastructure/analysis/utils/json_utils.py index c27b563..5980b35 100644 --- a/src/infrastructure/analysis/utils/json_utils.py +++ b/src/infrastructure/analysis/utils/json_utils.py @@ -9,6 +9,57 @@ from ....utils.logger import logger +def _extract_json_balanced(text: str, open_char: str, close_char: str) -> str | None: + """ + Extract the first complete JSON array or object from text, handling strings correctly. + + This function properly handles: + - Strings containing ] or } characters + - Escaped characters within strings + - Nested structures + + Args: + text: Text containing JSON + open_char: Opening bracket/brace ('[' or '{') + close_char: Closing bracket/brace (']' or '}') + + Returns: + The extracted JSON string, or None if not found + """ + start = text.find(open_char) + if start == -1: + return None + + depth = 0 + in_string = False + escape_next = False + + for i, char in enumerate(text[start:], start): + if escape_next: + escape_next = False + continue + + if char == "\\": + escape_next = True + continue + + if char == '"' and not escape_next: + in_string = not in_string + continue + + if in_string: + continue + + if char == open_char: + depth += 1 + elif char == close_char: + depth -= 1 + if depth == 0: + return text[start : i + 1] + + return None + + def fix_json(text: str) -> str: """ 修复JSON格式问题,包括中文符号替换 @@ -28,49 +79,32 @@ def fix_json(text: str) -> str: text = text.replace("\n", " ").replace("\r", " ") text = re.sub(r"\s+", " ", text) - # 3. 替换中文符号为英文符号(修复) - # 中文引号 -> 英文引号 - text = text.replace("“", '"').replace("”", '"') - text = text.replace("‘", "'").replace("’", "'") - # 中文逗号 -> 英文逗号 - text = text.replace(",", ",") - # 中文冒号 -> 英文冒号 - text = text.replace(":", ":") - # 中文括号 -> 英文括号 - text = text.replace("(", "(").replace(")", ")") - text = text.replace("【", "[").replace("】", "]") - - # 4. 处理字符串内容中的特殊字符 - # 转义字符串内的双引号 - def escape_quotes_in_strings(match): - content = match.group(1) - # 转义内部的双引号 - content = content.replace('"', '\\"') - return f'"{content}"' - - # 先处理字段值中的引号 - text = re.sub(r'"([^"]*(?:"[^"]*)*)"', escape_quotes_in_strings, text) - - # 5. 修复截断的JSON + # 3. 替换中文符号为英文符号 + text = text.replace("\u201c", '"').replace("\u201d", '"') + text = text.replace("\u2018", "'").replace("\u2019", "'") + text = text.replace("\uff0c", ",") + text = text.replace("\uff1a", ":") + text = text.replace("\uff08", "(").replace("\uff09", ")") + text = text.replace("\u3010", "[").replace("\u3011", "]") + + # 4. 修复截断的JSON if not text.endswith("]"): last_complete = text.rfind("}") if last_complete > 0: text = text[: last_complete + 1] + "]" - # 6. 修复常见的JSON格式问题 - # 1. 修复缺失的逗号 + # 5. 修复缺失的逗号 text = re.sub(r"}\s*{", "}, {", text) - # 2. 确保字段名有引号(仅在对象开始或逗号后,避免破坏字符串值) + # 6. 确保字段名有引号 def quote_field_names(match): prefix = match.group(1) key = match.group(2) return f'{prefix}"{key}":' - # 只在 { 或 , 后面匹配字段名,避免在字符串值中误匹配 text = re.sub(r"([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:", quote_field_names, text) - # 3. 移除多余的逗号 + # 7. 移除多余的逗号 text = re.sub(r",\s*}", "}", text) text = re.sub(r",\s*]", "]", text) @@ -96,14 +130,13 @@ def parse_json_response( """ fixed_json_text = None try: - # 1. 提取JSON部分 - json_match = re.search(r"\[.*?\]", result_text, re.DOTALL) - if not json_match: + # 1. 提取JSON数组(使用balanced extraction处理字符串内的]字符) + json_text = _extract_json_balanced(result_text, "[", "]") + if not json_text: error_msg = f"{data_type}响应中未找到JSON格式" logger.warning(error_msg) return False, None, error_msg - json_text = json_match.group() logger.debug(f"{data_type}分析JSON原文: {json_text[:500]}...") # 2. 尝试直接解析 @@ -162,17 +195,16 @@ def parse_json_object_response( raw_text = re.sub(r"```\s*$", "", raw_text) raw_text = raw_text.strip() - # 2. 提取 JSON 对象 - json_match = re.search(r"\{.*\}", raw_text, re.DOTALL) - if not json_match: + # 2. 提取 JSON 对象(使用balanced extraction处理字符串内的}字符) + json_text = _extract_json_balanced(raw_text, "{", "}") + if not json_text: error_msg = f"{data_type}响应中未找到JSON对象" logger.warning(error_msg) return False, None, error_msg - json_text = json_match.group() logger.debug(f"{data_type}分析JSON原文: {json_text[:500]}...") - # 3. 尝试直接解析(保留原始文本,避免中文引号被破坏) + # 3. 尝试直接解析 try: data = json.loads(json_text) logger.info(f"{data_type}直接解析成功") @@ -182,10 +214,10 @@ def parse_json_object_response( # 4. 使用 fix_json 修复后重试 fixed_json = fix_json(json_text) - fixed_match = re.search(r"\{.*\}", fixed_json, re.DOTALL) - if fixed_match: + fixed_text = _extract_json_balanced(fixed_json, "{", "}") + if fixed_text: try: - data = json.loads(fixed_match.group()) + data = json.loads(fixed_text) logger.info(f"{data_type}修复后解析成功") return True, data, None except json.JSONDecodeError as e: diff --git a/src/infrastructure/config/config_manager.py b/src/infrastructure/config/config_manager.py index b671192..1c5c959 100644 --- a/src/infrastructure/config/config_manager.py +++ b/src/infrastructure/config/config_manager.py @@ -466,15 +466,6 @@ def get_enable_user_card(self) -> bool: """获取是否使用用户群名片""" return self._get_group("basic").get("enable_user_card", False) - def get_enable_analysis_reply(self) -> bool: - """获取是否在群分析完成后发送文本回复""" - return self._get_group("basic").get("enable_analysis_reply", False) - - def set_enable_analysis_reply(self, enabled: bool): - """设置是否在群分析完成后发送文本回复""" - self._ensure_group("basic")["enable_analysis_reply"] = enabled - self.config.save_config() - # ========== 群文件/群相册上传配置 ========== def get_enable_group_file_upload(self) -> bool: diff --git a/src/infrastructure/reporting/templates/format/image_template.html b/src/infrastructure/reporting/templates/format/image_template.html index abd7104..6a60517 100644 --- a/src/infrastructure/reporting/templates/format/image_template.html +++ b/src/infrastructure/reporting/templates/format/image_template.html @@ -27,6 +27,9 @@ } body { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; background-color: var(--bg-body); color: var(--text-main); @@ -77,6 +80,9 @@ } .section { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; margin-bottom: 50px; } @@ -85,6 +91,9 @@ } .section-title { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: 'Noto Serif SC', serif; font-size: 1.5em; font-weight: 600; @@ -95,6 +104,9 @@ } .section-title::before { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; content: ''; position: absolute; left: 0; @@ -136,6 +148,8 @@ } .stat-label { + word-wrap: break-word; + word-break: break-all; font-size: 0.75em; color: var(--text-muted); font-weight: 500; @@ -280,6 +294,9 @@ } .topic-title { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: 'Noto Serif SC', serif; font-weight: 600; color: var(--text-main); @@ -296,6 +313,9 @@ } .topic-detail { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; color: #444; line-height: 1.6; font-size: 0.9em; @@ -382,6 +402,9 @@ } .user-reason { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; color: var(--text-muted); font-size: 0.85em; font-weight: 500; @@ -401,6 +424,9 @@ } .quote-content { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: 'Noto Serif SC', serif; font-size: 1.1em; color: var(--text-main); @@ -419,6 +445,9 @@ } .quote-reason { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-size: 0.75em; color: var(--text-muted); background: rgba(0, 0, 0, 0.03); @@ -443,6 +472,9 @@ /* Responsive */ @media (max-width: 600px) { body { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; padding: 15px; } @@ -468,6 +500,9 @@ } .user-reason { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; padding-left: 0; margin-top: 10px; } diff --git a/src/infrastructure/reporting/templates/format/quote_item.html b/src/infrastructure/reporting/templates/format/quote_item.html index 8a6d3b5..9ae49a2 100644 --- a/src/infrastructure/reporting/templates/format/quote_item.html +++ b/src/infrastructure/reporting/templates/format/quote_item.html @@ -5,7 +5,7 @@

群圣经

"{{ quote.content }}"
—— {{ quote.sender }}
-
{{ quote.reason }}
+
{{ quote.reason | safe }}
{% endfor %} diff --git a/src/infrastructure/reporting/templates/hack/image_template.html b/src/infrastructure/reporting/templates/hack/image_template.html index 3bec608..b1252bf 100644 --- a/src/infrastructure/reporting/templates/hack/image_template.html +++ b/src/infrastructure/reporting/templates/hack/image_template.html @@ -35,6 +35,9 @@ } body { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; margin: 0; background-color: var(--bg-deep); color: var(--text-primary); @@ -50,6 +53,9 @@ } body::before { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; content: ""; position: fixed; top: 0; @@ -158,6 +164,9 @@ } .comment { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; color: var(--text-secondary); } @@ -219,6 +228,9 @@ } .stat-label { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: var(--font-mono); color: var(--text-secondary); font-size: 0.85rem; @@ -229,11 +241,17 @@ } .stat-label::before { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; content: '>'; color: var(--accent-orange); } .stat-value { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-size: 3.5rem; font-weight: 800; color: var(--accent-orange); @@ -263,6 +281,9 @@ } .section-header { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: var(--font-mono); font-size: 1.1rem; margin-bottom: 25px; @@ -274,6 +295,9 @@ /* Code Block Container */ .code-block { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; background: rgba(20, 20, 20, 0.8); border: 1px solid rgba(255, 153, 0, 0.15); padding: 30px; @@ -400,11 +424,17 @@ } .user-info h4 { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; margin: 0 0 5px; font-size: 1.1rem; } .tag { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: var(--font-mono); font-size: 0.75rem; padding: 2px 8px; @@ -414,12 +444,18 @@ } .tag.mbti { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; color: #ffcc00; border-color: rgba(255, 204, 0, 0.3); background: rgba(255, 204, 0, 0.05); } .tag.title { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; color: var(--accent-blue); border-color: rgba(51, 153, 255, 0.3); background: rgba(51, 153, 255, 0.05); @@ -438,6 +474,9 @@ } .topic-title { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-weight: bold; font-size: 1.5rem; margin-bottom: 12px; @@ -477,6 +516,9 @@ } .quote-text { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-style: italic; font-size: 1.1rem; color: var(--accent-orange); @@ -493,6 +535,9 @@ /* Footer */ footer { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; margin-top: 80px; padding-top: 40px; border-top: 1px solid var(--border-color); diff --git a/src/infrastructure/reporting/templates/hack/quote_item.html b/src/infrastructure/reporting/templates/hack/quote_item.html index 664ee34..f746ddc 100644 --- a/src/infrastructure/reporting/templates/hack/quote_item.html +++ b/src/infrastructure/reporting/templates/hack/quote_item.html @@ -16,7 +16,7 @@
"{{ quote.content }}"
-- {{ quote.sender }}
-
// Reason: {{ quote.reason }}
+
// Reason: {{ quote.reason | safe }}
{% endfor %} diff --git a/src/infrastructure/reporting/templates/retro_futurism/image_template.html b/src/infrastructure/reporting/templates/retro_futurism/image_template.html index 7d8176a..def84fe 100644 --- a/src/infrastructure/reporting/templates/retro_futurism/image_template.html +++ b/src/infrastructure/reporting/templates/retro_futurism/image_template.html @@ -58,6 +58,9 @@ } body { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; margin: 0; background-color: var(--c-bg); color: var(--c-text); @@ -315,6 +318,9 @@ /* Section Labels */ .section-label { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; grid-column: span 12; font-family: var(--font-display); font-size: 3.5rem; @@ -329,6 +335,9 @@ } .section-label span { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; background: var(--c-text); color: var(--c-bg); padding: 8px 20px; @@ -348,6 +357,9 @@ } .stat-label { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: var(--font-mono); font-size: 0.9rem; font-weight: bold; @@ -359,6 +371,9 @@ } .stat-value { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: var(--font-display); font-size: 5rem; color: var(--c-accent); @@ -476,6 +491,9 @@ } .topic-title { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: var(--font-display); font-size: 3rem; margin: 0 0 20px; @@ -493,6 +511,9 @@ } .topic-detail { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-size: 1.4rem; line-height: 1.8; color: #333; @@ -557,6 +578,9 @@ } .title-reason { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-size: 1.2rem; line-height: 1.6; margin-top: 15px; @@ -601,6 +625,9 @@ } .quote-content { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: "JetBrains Mono", monospace; font-size: 1.6rem; line-height: 1.6; @@ -610,6 +637,9 @@ } .quote-content::before { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; content: '"'; position: absolute; left: -20px; @@ -628,6 +658,9 @@ } .quote-reason-small { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-size: 1rem; color: #666; margin-top: 10px; @@ -636,6 +669,9 @@ /* Footer */ footer { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; margin-top: 100px; padding: 60px 0; border-top: 8px solid var(--c-text); diff --git a/src/infrastructure/reporting/templates/retro_futurism/quote_item.html b/src/infrastructure/reporting/templates/retro_futurism/quote_item.html index a897760..c6db3ed 100644 --- a/src/infrastructure/reporting/templates/retro_futurism/quote_item.html +++ b/src/infrastructure/reporting/templates/retro_futurism/quote_item.html @@ -13,7 +13,7 @@
{{ quote.content }}
ORIGIN: {{ quote.sender }}
-
ANALYSIS // {{ quote.reason }}
+
ANALYSIS // {{ quote.reason | safe }}
{% endfor %} diff --git a/src/infrastructure/reporting/templates/scrapbook/image_template.html b/src/infrastructure/reporting/templates/scrapbook/image_template.html index e684c1e..9c17fa2 100644 --- a/src/infrastructure/reporting/templates/scrapbook/image_template.html +++ b/src/infrastructure/reporting/templates/scrapbook/image_template.html @@ -161,6 +161,9 @@ } body { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: var(--font-body); color: var(--ink-primary); background-color: var(--bg-paper); @@ -340,6 +343,9 @@ } .section-title { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: var(--font-title); font-size: 1.5rem; margin-bottom: 15px; @@ -349,6 +355,9 @@ } .section-title .doodle { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; color: var(--accent-orange); font-size: 1.3em; } @@ -557,6 +566,9 @@ } .topic-title { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: var(--font-title); font-size: 1.5rem; margin-right: 10px; @@ -565,14 +577,23 @@ } .topic-item:nth-child(even) .topic-title { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; background: linear-gradient(transparent 60%, var(--color-blue) 60%); } .topic-item:nth-child(3n) .topic-title { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; background: linear-gradient(transparent 60%, var(--color-green) 60%); } .topic-detail { + word-wrap: break-word; + word-break: break-all; + overflow: hidden; font-family: var(--font-hand); color: #666; font-size: 1.2rem; diff --git a/src/infrastructure/reporting/templates/simple/image_template.html b/src/infrastructure/reporting/templates/simple/image_template.html index 3453944..a12b592 100644 --- a/src/infrastructure/reporting/templates/simple/image_template.html +++ b/src/infrastructure/reporting/templates/simple/image_template.html @@ -6,11 +6,17 @@ Simple Report