"
metadata = {}
# Build container
parts = []
# Container styles
styles = ["position: relative"]
if max_height:
styles.append(f"max-height: {max_height}px")
styles.append("overflow-y: auto")
style_str = "; ".join(styles)
# Main container with bbox mode
parts.append(
f'
'
)
# Bounding box toolbar
parts.append('''
Boxes: 0
''')
# Content container with bbox canvas overlay
parts.append('
')
# The actual document content
parts.append(f'
{rendered_html}
')
# Canvas overlay for drawing bounding boxes
parts.append('')
# Hidden input that carries the drawn boxes through the standard save
# pipeline. saveAnnotations() collects any input.annotation-data-input as
# "{name}:::_data", the server stores it, and render_page_with_annotations
# repopulates value + data-server-set on restore — the same channel the
# image_annotation/video schemas use. Without this the boxes are never
# persisted (F-040). The name matches data-field-key so document-bbox.js
# reads/writes it.
parts.append(
f''
)
parts.append('
') # Close bbox-container
# Metadata footer
if metadata:
parts.append(self._render_metadata(metadata))
parts.append('
') # Close main container
return "\n".join(parts)
def _render_outline(self, headings: List[Dict[str, Any]]) -> str:
"""
Render document outline/table of contents.
"""
if not headings:
return ""
parts = ['')
return "\n".join(parts)
def _render_metadata(self, metadata: Dict[str, Any]) -> str:
"""
Render metadata footer.
"""
info_items = []
if "format" in metadata:
info_items.append(f"Format: {metadata['format'].upper()}")
if "paragraph_count" in metadata or "paragraphs" in metadata:
count = len(metadata.get("paragraphs", [])) or metadata.get("paragraph_count", 0)
if count:
info_items.append(f"Paragraphs: {count}")
if "line_count" in metadata:
info_items.append(f"Lines: {metadata['line_count']}")
if "char_count" in metadata:
info_items.append(f"Characters: {metadata['char_count']:,}")
if not info_items:
return ""
info_str = " | ".join(info_items)
return f'
{info_str}
'
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-document")
if options.get("collapsible"):
classes.append("document-collapsible-enabled")
theme = options.get("style_theme", "default")
classes.append(f"document-theme-{theme}")
annotation_mode = options.get("annotation_mode", "span")
if annotation_mode == "bounding_box":
classes.append("document-bbox-annotation")
return classes
def validate_config(self, field_config: Dict[str, Any]) -> List[str]:
"""Validate the field configuration."""
errors = super().validate_config(field_config)
options = field_config.get("display_options", {})
# Validate annotation_mode
valid_modes = ["span", "bounding_box"]
annotation_mode = options.get("annotation_mode", "span")
if annotation_mode not in valid_modes:
errors.append(
f"Invalid annotation_mode '{annotation_mode}'. "
f"Must be one of: {', '.join(valid_modes)}"
)
# Validate style_theme
valid_themes = ["default", "minimal", "print"]
theme = options.get("style_theme", "default")
if theme not in valid_themes:
errors.append(
f"Invalid style_theme '{theme}'. "
f"Must be one of: {', '.join(valid_themes)}"
)
return errors
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)
# Add format type if available
if isinstance(data, dict) and "format_name" in data:
attrs["format"] = data["format_name"]
return attrs
def has_inline_label(self, field_config: Dict[str, Any]) -> bool:
"""
Check if the display handles its own label.
For collapsible documents, the label is shown in the summary element.
"""
options = self.get_display_options(field_config)
return options.get("collapsible", False)