| import json |
| from typing import List |
| from dataclasses import asdict |
| import re |
|
|
| from .types import SearchResult, StackOverflowAnswer, StackOverflowComment |
|
|
|
|
| def format_response(results: List[SearchResult], format_type: str = "markdown") -> str: |
| """Format search results as either JSON or Markdown. |
| |
| Args: |
| results (List[SearchResult]): List of search results to format |
| format_type (str, optional): Output format type - either "json" or "markdown". Defaults to "markdown". |
| |
| Returns: |
| str: Formatted string representation of the search results |
| """ |
| |
| if format_type == "json": |
| def _convert_to_dict(obj): |
| if hasattr(obj, "__dataclass_fields__"): |
| return asdict(obj) |
| return obj |
| |
| class DataClassJSONEncoder(json.JSONEncoder): |
| def default(self, obj): |
| if hasattr(obj, "__dataclass_fields__"): |
| return asdict(obj) |
| return super().default(obj) |
| |
| return json.dumps(results, cls=DataClassJSONEncoder, indent=2) |
| |
| if not results: |
| return "No results found." |
| |
| markdown = "" |
| |
| for result in results: |
| markdown += f"# {result.question.title}\n\n" |
| markdown += f"**Score:** {result.question.score} | **Answers:** {result.question.answer_count}\n\n" |
| |
| question_body = clean_html(result.question.body) |
| markdown += f"## Question\n\n{question_body}\n\n" |
| |
| if result.comments and result.comments.question: |
| markdown += "### Question Comments\n\n" |
| for comment in result.comments.question: |
| markdown += f"- {clean_html(comment.body)} *(Score: {comment.score})*\n" |
| markdown += "\n" |
| |
| markdown += "## Answers\n\n" |
| for answer in result.answers: |
| markdown += f"### {'✓ ' if answer.is_accepted else ''}Answer (Score: {answer.score})\n\n" |
| answer_body = clean_html(answer.body) |
| markdown += f"{answer_body}\n\n" |
| |
| if (result.comments and |
| result.comments.answers and |
| answer.answer_id in result.comments.answers and |
| result.comments.answers[answer.answer_id] |
| ): |
| markdown += "#### Answer Comments\n\n" |
| for comment in result.comments.answers[answer.answer_id]: |
| markdown += f"- {clean_html(comment.body)} *(Score: {comment.score})*\n" |
| |
| markdown += "/n" |
| |
| markdown += f"---\n\n[View on Stack Overflow]({result.question.link})\n\n" |
| |
| return markdown |
|
|
| def clean_html(html_text: str) -> str: |
| """Clean HTML tags from text while preserving code blocks. |
| |
| Args: |
| html_text (str): HTML text to be cleaned |
| |
| Returns: |
| str: Cleaned text with HTML tags removed and code blocks preserved |
| """ |
| |
| code_blocks = [] |
| |
| def replace_code_block(match): |
| code = match.group(1) or match.group(2) |
| code_blocks.append(code) |
| return f"CODE_BLOCK_{len(code_blocks)-1}" |
| |
| html_without_code = re.sub(r'<pre><code>(.*?)</code></pre>|<code>(.*?)</code>', replace_code_block, html_text, flags=re.DOTALL) |
| |
| text_without_html = re.sub(r'<[^>]+>', '', html_without_code) |
| |
| for i, code in enumerate(code_blocks): |
| if '\n' in code or len(code) > 80: |
| text_without_html = text_without_html.replace(f"CODE_BLOCK_{i}", f"```\n{code}\n```") |
| else: |
| text_without_html = text_without_html.replace(f"CODE_BLOCK_{i}", f"`{code}`") |
| |
| |
| text_without_html = text_without_html.replace("<", "<") |
| text_without_html = text_without_html.replace(">", ">") |
| text_without_html = text_without_html.replace("&", "&") |
| text_without_html = text_without_html.replace(""", "\"") |
| |
| return text_without_html |