Spaces:
Paused
Paused
File size: 9,026 Bytes
aceb1b2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | """
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, # Language for syntax highlighting
"show_line_numbers": True, # Show line numbers
"max_height": 500, # Max container height
"max_width": None, # Max container width
"wrap_lines": False, # Wrap long lines
"highlight_lines": None, # List of line numbers to highlight
"start_line": 1, # Starting line number
"theme": "default", # Color theme
"copy_button": True, # Show copy to clipboard button
}
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")
# Handle different data formats
if isinstance(data, dict):
# Pre-extracted FormatOutput data
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>'
# Generate code HTML
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.
"""
# For span targets, use simpler rendering for accurate position calculation
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 ""
# Escape code for HTML but preserve structure
escaped_code = html.escape(code)
# Don't use data-original-text - let span system use DOM textContent
# This ensures canonical text matches what user sees and selects
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();
}
'''
|