| """ |
| Code Display Component |
| |
| Renders source code with syntax highlighting and line number support. |
| |
| Usage: |
| In instance_display config: |
| fields: |
| - key: source_code |
| type: code |
| display_options: |
| language: python |
| show_line_numbers: true |
| max_height: 500 |
| """ |
|
|
| from typing import Dict, Any, List, Optional |
| import html |
| import logging |
|
|
| from .base import BaseDisplay |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class CodeDisplay(BaseDisplay): |
| """ |
| Display type for source code with syntax highlighting. |
| |
| Uses Pygments for server-side highlighting or can use client-side |
| highlighting with highlight.js. |
| """ |
|
|
| name = "code" |
| required_fields = ["key"] |
| optional_fields = { |
| "language": None, |
| "show_line_numbers": True, |
| "max_height": 500, |
| "max_width": None, |
| "wrap_lines": False, |
| "highlight_lines": None, |
| "start_line": 1, |
| "theme": "default", |
| "copy_button": True, |
| } |
| description = "Source code display with syntax highlighting" |
| supports_span_target = True |
|
|
| def render(self, field_config: Dict[str, Any], data: Any) -> str: |
| """ |
| Render source code content. |
| |
| Args: |
| field_config: Display configuration |
| data: Either a dict with extracted content or raw code string |
| |
| Returns: |
| HTML string for rendering |
| """ |
| options = self.get_display_options(field_config) |
| field_key = field_config.get("key", "code") |
|
|
| |
| if isinstance(data, dict): |
| |
| if "rendered_html" in data: |
| return self._wrap_content(data["rendered_html"], options, field_key) |
|
|
| code = data.get("text", "") |
| language = data.get("metadata", {}).get("language", options.get("language")) |
| elif isinstance(data, str): |
| code = data |
| language = options.get("language") |
| else: |
| return f'<div class="code-error">Unsupported content type</div>' |
|
|
| |
| is_span_target = field_config.get("span_target", False) |
| code_html = self._render_code(code, language, options, field_key, is_span_target) |
| return self._wrap_content(code_html, options, field_key) |
|
|
| def _wrap_content( |
| self, |
| content: str, |
| options: Dict[str, Any], |
| field_key: str |
| ) -> str: |
| """ |
| Wrap code content in container with styles. |
| """ |
| styles = [] |
| max_height = options.get("max_height") |
| max_width = options.get("max_width") |
|
|
| if max_height: |
| styles.append(f"max-height: {max_height}px") |
| styles.append("overflow-y: auto") |
| if max_width: |
| styles.append(f"max-width: {max_width}px") |
| styles.append("overflow-x: auto") |
|
|
| style_str = "; ".join(styles) if styles else "" |
| language = options.get("language") or "text" |
| theme = options.get("theme", "default") |
|
|
| return f''' |
| <div class="code-display code-theme-{theme}" |
| data-field-key="{field_key}" |
| data-language="{language}" |
| style="{style_str}"> |
| {self._render_copy_button() if options.get("copy_button") else ""} |
| {content} |
| </div> |
| ''' |
|
|
| def _render_copy_button(self) -> str: |
| """ |
| Render copy to clipboard button. |
| """ |
| return ''' |
| <button type="button" class="code-copy-btn" title="Copy to clipboard"> |
| <svg viewBox="0 0 24 24" width="16" height="16"> |
| <path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/> |
| </svg> |
| </button> |
| ''' |
|
|
| def _render_code( |
| self, |
| code: str, |
| language: Optional[str], |
| options: Dict[str, Any], |
| field_key: str = "code", |
| is_span_target: bool = False |
| ) -> str: |
| """ |
| Render code with line numbers and optional highlighting. |
| |
| When is_span_target is True, uses a simpler pre/code structure |
| that works better with span annotation positioning. |
| """ |
| |
| if is_span_target: |
| return self._render_code_simple(code, language, options, field_key) |
|
|
| lines = code.split("\n") |
| parts = [] |
|
|
| show_line_numbers = options.get("show_line_numbers", True) |
| start_line = options.get("start_line", 1) |
| highlight_lines = set(options.get("highlight_lines") or []) |
| wrap_lines = options.get("wrap_lines", False) |
|
|
| wrap_class = "code-wrap" if wrap_lines else "code-nowrap" |
| lang_class = f"language-{language}" if language else "" |
|
|
| content_classes = ["code-content", wrap_class] |
| content_class_str = " ".join(content_classes) |
|
|
| parts.append(f'<div class="{content_class_str}" id="text-content-{field_key}">') |
| parts.append(f'<table class="code-table {lang_class}">') |
|
|
| for i, line in enumerate(lines): |
| line_num = i + start_line |
| line_classes = ["code-line"] |
|
|
| if line_num in highlight_lines: |
| line_classes.append("highlighted-line") |
|
|
| escaped_line = html.escape(line) if line else " " |
|
|
| parts.append(f'<tr id="L{line_num}" class="{" ".join(line_classes)}">') |
|
|
| if show_line_numbers: |
| parts.append( |
| f'<td class="line-number" ' |
| f'data-line="{line_num}">{line_num}</td>' |
| ) |
|
|
| parts.append( |
| f'<td class="line-content">' |
| f'<code>{escaped_line}</code>' |
| f'</td>' |
| ) |
|
|
| parts.append('</tr>') |
|
|
| parts.append('</table>') |
| parts.append('</div>') |
|
|
| return "\n".join(parts) |
|
|
| def _render_code_simple( |
| self, |
| code: str, |
| language: Optional[str], |
| options: Dict[str, Any], |
| field_key: str |
| ) -> str: |
| """ |
| Render code in a simple pre/code format for span annotation. |
| |
| This avoids table structures that interfere with text position calculations. |
| Uses a flat text structure without data-original-text so the span system |
| uses DOM textContent directly (which matches what the user sees and selects). |
| """ |
| lang_class = f"language-{language}" if language else "" |
|
|
| |
| escaped_code = html.escape(code) |
|
|
| |
| |
| return f''' |
| <div class="code-content code-simple text-content" id="text-content-{field_key}"> |
| <pre class="code-pre {lang_class}"><code>{escaped_code}</code></pre> |
| </div> |
| ''' |
|
|
| def get_css_classes(self, field_config: Dict[str, Any]) -> List[str]: |
| """Get CSS classes for the display container.""" |
| classes = super().get_css_classes(field_config) |
| options = self.get_display_options(field_config) |
|
|
| if field_config.get("span_target"): |
| classes.append("span-target-code") |
|
|
| language = options.get("language") |
| if language: |
| classes.append(f"language-{language}") |
|
|
| theme = options.get("theme", "default") |
| classes.append(f"code-theme-{theme}") |
|
|
| return classes |
|
|
| def get_data_attributes( |
| self, |
| field_config: Dict[str, Any], |
| data: Any |
| ) -> Dict[str, str]: |
| """Get data attributes for JavaScript initialization.""" |
| attrs = super().get_data_attributes(field_config, data) |
| options = self.get_display_options(field_config) |
|
|
| language = options.get("language") |
| if language: |
| attrs["language"] = language |
|
|
| if isinstance(data, dict) and "metadata" in data: |
| meta = data["metadata"] |
| if "language" in meta: |
| attrs["language"] = meta["language"] |
| if "line_count" in meta: |
| attrs["line-count"] = str(meta["line_count"]) |
|
|
| return attrs |
|
|
| def get_js_init(self) -> Optional[str]: |
| """ |
| Return JavaScript initialization code for code displays. |
| """ |
| return ''' |
| if (typeof initCodeDisplays === 'function') { |
| initCodeDisplays(); |
| } |
| ''' |
|
|