from __future__ import annotations import re from yuxi.channel.security.html_guard import sanitize_text_content_preserve_entities _TEXT_CONTENT_RE = re.compile(r'{"text":"((?:[^"\\]|\\.)*)"}') _ESCAPE_RE = re.compile(r"([\\*_{}\[\]()#+\-!|>~])") def markdown_to_feishu_post(md_text: str) -> dict: lines = md_text.split("\n") elements: list[dict] = [] i = 0 while i < len(lines): line = lines[i] if line.startswith("```"): language = line[3:].strip() code_lines = [] i += 1 while i < len(lines) and not lines[i].startswith("```"): code_lines.append(lines[i]) i += 1 i += 1 elements.append(_build_code_block("\n".join(code_lines), language)) continue if line.startswith("### "): elements.append(_build_paragraph([_build_text(line[4:], bold=True)])) elif line.startswith("## "): elements.append(_build_paragraph([_build_text(line[3:], bold=True)])) elif line.startswith("# "): elements.append(_build_paragraph([_build_text(line[2:], bold=True)])) elif line.startswith("> "): content = line[2:] content = _ESCAPE_RE.sub(r"\\\1", content) elements.append(_build_paragraph([_build_text(content)])) elif line.startswith("- ") or line.startswith("* "): content = _parse_inline_markdown(line[2:]) elements.append(_build_paragraph([_build_text("• " + content)])) elif re.match(r"^\d+\.\s", line): content = _parse_inline_markdown(re.sub(r"^\d+\.\s", "", line)) elements.append(_build_paragraph([_build_text(content)])) elif line.strip() == "": pass else: content = _parse_inline_markdown(line) if content: elements.append(_build_paragraph([_build_text(content)])) i += 1 if not elements: elements.append(_build_paragraph([_build_text("")])) return { "zh_cn": { "title": "", "content": [elements], } } def md_to_plain_text(md_text: str) -> str: text = md_text text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) text = re.sub(r"__([^_]+)__", r"\1", text) text = re.sub(r"\*([^*]+)\*", r"\1", text) text = re.sub(r"_([^_]+)_", r"\1", text) text = re.sub(r"~~(.+?)~~", r"\1", text) text = re.sub(r"```(\w*)\n(.*?)```", r"\2", text, flags=re.DOTALL) text = re.sub(r"`([^`]+)`", r"\1", text) text = re.sub(r"\[([^\]]*)\]\([^)]+\)", r"\1", text) text = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"[\1]", text) text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE) text = re.sub(r"^>\s+", "", text, flags=re.MULTILINE) text = re.sub(r"^[*-]\s+", "• ", text, flags=re.MULTILINE) return text.strip() def _parse_inline_markdown(text: str) -> str: result = _ESCAPE_RE.sub(r"\\\1", text) result = re.sub(r"\*\*(.+?)\*\*", r"\1", result) result = re.sub(r"__([^_]+)__", r"\1", result) result = re.sub(r"\*([^*]+)\*", r"\1", result) result = re.sub(r"`([^`]+)`", r"\1", result) result = re.sub(r"~~(.+?)~~", r"\1", result) return result def _build_text(content: str, bold: bool = False) -> dict: return { "tag": "text", "text": sanitize_text_content_preserve_entities(content), "style": ["bold"] if bold else [], } def _build_paragraph(elements: list[dict]) -> dict: return { "tag": "p", "children": elements if elements else [_build_text("")], } def _build_code_block(code: str, language: str = "") -> dict: return { "tag": "pre", "children": [{ "tag": "code", "code": sanitize_text_content_preserve_entities(code), "code_language": sanitize_text_content_preserve_entities(language) or "plaintext", }], } def chunk_post_content(elements: list[list[dict]], limit: int = 28000) -> list[list[list[dict]]]: chunks: list[list[list[dict]]] = [] current: list[list[dict]] = [] current_size = 0 for para_elements in elements: para_size = _estimate_paragraph_size(para_elements) if current_size + para_size > limit and current: chunks.append(current) current = [] current_size = 0 current.append(para_elements) current_size += para_size if current: chunks.append(current) return chunks if chunks else [elements] def _estimate_paragraph_size(para_elements: list[dict]) -> int: size = 0 for elem in para_elements: if isinstance(elem, dict): size += len(str(elem.get("text", ""))) + 50 return size def text_to_post_message(text: str) -> str: import json post = markdown_to_feishu_post(text) return json.dumps(post, ensure_ascii=False)