757 lines
25 KiB
Python
757 lines
25 KiB
Python
from __future__ import annotations
|
|
from typing import List, Dict, Optional, Tuple, Union, Any
|
|
from enum import Enum
|
|
from .block import Block, BlockType, Heading, HeadingLevel, Paragraph
|
|
from .functional import Link, Button, Form
|
|
from .inline import Word, FormattedSpan
|
|
from ..style import Font, FontWeight, FontStyle, TextDecoration
|
|
from ..style.abstract_style import AbstractStyle, AbstractStyleRegistry, FontFamily, FontSize
|
|
from ..style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
|
|
|
|
|
|
class MetadataType(Enum):
|
|
"""Types of metadata that can be associated with a document"""
|
|
TITLE = 1
|
|
AUTHOR = 2
|
|
DESCRIPTION = 3
|
|
KEYWORDS = 4
|
|
LANGUAGE = 5
|
|
PUBLICATION_DATE = 6
|
|
MODIFIED_DATE = 7
|
|
PUBLISHER = 8
|
|
IDENTIFIER = 9
|
|
COVER_IMAGE = 10
|
|
CUSTOM = 100
|
|
|
|
|
|
class Document:
|
|
"""
|
|
Abstract representation of a complete document like an HTML page or an ebook.
|
|
This class manages the logical structure of the document without rendering concerns.
|
|
"""
|
|
|
|
def __init__(self, title: Optional[str] = None, language: str = "en-US", default_style=None):
|
|
"""
|
|
Initialize a new document.
|
|
|
|
Args:
|
|
title: The document title
|
|
language: The document language code
|
|
default_style: Optional default style for child blocks
|
|
"""
|
|
self._blocks: List[Block] = []
|
|
self._metadata: Dict[MetadataType, Any] = {}
|
|
self._anchors: Dict[str, Block] = {} # Named anchors for navigation
|
|
self._resources: Dict[str, Any] = {} # External resources like images
|
|
self._stylesheets: List[Dict[str, Any]] = [] # CSS stylesheets
|
|
self._scripts: List[str] = [] # JavaScript code
|
|
self._fonts: Dict[str, Font] = {} # Font registry for backward compatibility
|
|
|
|
# Style management with new abstract/concrete system
|
|
self._abstract_style_registry = AbstractStyleRegistry()
|
|
self._rendering_context = RenderingContext(default_language=language)
|
|
self._style_resolver = StyleResolver(self._rendering_context)
|
|
self._concrete_style_registry = ConcreteStyleRegistry(self._style_resolver)
|
|
|
|
# Set default style
|
|
if default_style is None:
|
|
# Create a default abstract style
|
|
default_style = self._abstract_style_registry.default_style
|
|
elif isinstance(default_style, Font):
|
|
# Convert Font to AbstractStyle for backward compatibility
|
|
default_style = AbstractStyle(
|
|
font_family=FontFamily.SERIF, # Default assumption
|
|
font_size=default_style.font_size,
|
|
color=default_style.colour,
|
|
language=default_style.language
|
|
)
|
|
style_id, default_style = self._abstract_style_registry.get_or_create_style(default_style)
|
|
self._default_style = default_style
|
|
|
|
# Set basic metadata
|
|
if title:
|
|
self.set_metadata(MetadataType.TITLE, title)
|
|
self.set_metadata(MetadataType.LANGUAGE, language)
|
|
|
|
@property
|
|
def blocks(self) -> List[Block]:
|
|
"""Get the top-level blocks in this document"""
|
|
return self._blocks
|
|
|
|
@property
|
|
def default_style(self):
|
|
"""Get the default style for this document"""
|
|
return self._default_style
|
|
|
|
@default_style.setter
|
|
def default_style(self, style):
|
|
"""Set the default style for this document"""
|
|
self._default_style = style
|
|
|
|
def add_block(self, block: Block):
|
|
"""
|
|
Add a block to this document.
|
|
|
|
Args:
|
|
block: The block to add
|
|
"""
|
|
self._blocks.append(block)
|
|
|
|
def create_paragraph(self, style=None) -> Paragraph:
|
|
"""
|
|
Create a new paragraph and add it to this document.
|
|
|
|
Args:
|
|
style: Optional style override. If None, inherits from document
|
|
|
|
Returns:
|
|
The newly created Paragraph object
|
|
"""
|
|
if style is None:
|
|
style = self._default_style
|
|
paragraph = Paragraph(style)
|
|
self.add_block(paragraph)
|
|
return paragraph
|
|
|
|
def create_heading(self, level: HeadingLevel = HeadingLevel.H1, style=None) -> Heading:
|
|
"""
|
|
Create a new heading and add it to this document.
|
|
|
|
Args:
|
|
level: The heading level
|
|
style: Optional style override. If None, inherits from document
|
|
|
|
Returns:
|
|
The newly created Heading object
|
|
"""
|
|
if style is None:
|
|
style = self._default_style
|
|
heading = Heading(level, style)
|
|
self.add_block(heading)
|
|
return heading
|
|
|
|
def create_chapter(self, title: Optional[str] = None, level: int = 1, style=None) -> 'Chapter':
|
|
"""
|
|
Create a new chapter with inherited style.
|
|
|
|
Args:
|
|
title: The chapter title
|
|
level: The chapter level
|
|
style: Optional style override. If None, inherits from document
|
|
|
|
Returns:
|
|
The newly created Chapter object
|
|
"""
|
|
if style is None:
|
|
style = self._default_style
|
|
return Chapter(title, level, style)
|
|
|
|
def set_metadata(self, meta_type: MetadataType, value: Any):
|
|
"""
|
|
Set a metadata value.
|
|
|
|
Args:
|
|
meta_type: The type of metadata
|
|
value: The metadata value
|
|
"""
|
|
self._metadata[meta_type] = value
|
|
|
|
def get_metadata(self, meta_type: MetadataType) -> Optional[Any]:
|
|
"""
|
|
Get a metadata value.
|
|
|
|
Args:
|
|
meta_type: The type of metadata
|
|
|
|
Returns:
|
|
The metadata value, or None if not set
|
|
"""
|
|
return self._metadata.get(meta_type)
|
|
|
|
def add_anchor(self, name: str, target: Block):
|
|
"""
|
|
Add a named anchor to this document.
|
|
|
|
Args:
|
|
name: The anchor name
|
|
target: The target block
|
|
"""
|
|
self._anchors[name] = target
|
|
|
|
def get_anchor(self, name: str) -> Optional[Block]:
|
|
"""
|
|
Get a named anchor from this document.
|
|
|
|
Args:
|
|
name: The anchor name
|
|
|
|
Returns:
|
|
The target block, or None if not found
|
|
"""
|
|
return self._anchors.get(name)
|
|
|
|
def add_resource(self, name: str, resource: Any):
|
|
"""
|
|
Add a resource to this document.
|
|
|
|
Args:
|
|
name: The resource name
|
|
resource: The resource data
|
|
"""
|
|
self._resources[name] = resource
|
|
|
|
def get_resource(self, name: str) -> Optional[Any]:
|
|
"""
|
|
Get a resource from this document.
|
|
|
|
Args:
|
|
name: The resource name
|
|
|
|
Returns:
|
|
The resource data, or None if not found
|
|
"""
|
|
return self._resources.get(name)
|
|
|
|
def add_stylesheet(self, stylesheet: Dict[str, Any]):
|
|
"""
|
|
Add a stylesheet to this document.
|
|
|
|
Args:
|
|
stylesheet: The stylesheet data
|
|
"""
|
|
self._stylesheets.append(stylesheet)
|
|
|
|
def add_script(self, script: str):
|
|
"""
|
|
Add a script to this document.
|
|
|
|
Args:
|
|
script: The script code
|
|
"""
|
|
self._scripts.append(script)
|
|
|
|
def get_title(self) -> Optional[str]:
|
|
"""
|
|
Get the document title.
|
|
|
|
Returns:
|
|
The document title, or None if not set
|
|
"""
|
|
return self.get_metadata(MetadataType.TITLE)
|
|
|
|
def set_title(self, title: str):
|
|
"""
|
|
Set the document title.
|
|
|
|
Args:
|
|
title: The document title
|
|
"""
|
|
self.set_metadata(MetadataType.TITLE, title)
|
|
|
|
@property
|
|
def title(self) -> Optional[str]:
|
|
"""
|
|
Get the document title as a property.
|
|
|
|
Returns:
|
|
The document title, or None if not set
|
|
"""
|
|
return self.get_title()
|
|
|
|
@title.setter
|
|
def title(self, title: str):
|
|
"""
|
|
Set the document title as a property.
|
|
|
|
Args:
|
|
title: The document title
|
|
"""
|
|
self.set_title(title)
|
|
|
|
def find_blocks_by_type(self, block_type: BlockType) -> List[Block]:
|
|
"""
|
|
Find all blocks of a specific type.
|
|
|
|
Args:
|
|
block_type: The type of blocks to find
|
|
|
|
Returns:
|
|
A list of matching blocks
|
|
"""
|
|
result = []
|
|
|
|
def _find_recursive(blocks: List[Block]):
|
|
for block in blocks:
|
|
if block.block_type == block_type:
|
|
result.append(block)
|
|
|
|
# Check for child blocks based on block type
|
|
if hasattr(block, '_blocks'):
|
|
_find_recursive(block._blocks)
|
|
elif hasattr(block, '_items') and isinstance(block._items, list):
|
|
_find_recursive(block._items)
|
|
|
|
_find_recursive(self._blocks)
|
|
return result
|
|
|
|
def find_headings(self) -> List[Heading]:
|
|
"""
|
|
Find all headings in the document.
|
|
|
|
Returns:
|
|
A list of heading blocks
|
|
"""
|
|
blocks = self.find_blocks_by_type(BlockType.HEADING)
|
|
return [block for block in blocks if isinstance(block, Heading)]
|
|
|
|
def generate_table_of_contents(self) -> List[Tuple[int, str, Block]]:
|
|
"""
|
|
Generate a table of contents from headings.
|
|
|
|
Returns:
|
|
A list of tuples containing (level, title, heading_block)
|
|
"""
|
|
headings = self.find_headings()
|
|
|
|
toc = []
|
|
for heading in headings:
|
|
# Extract text from the heading
|
|
title = ""
|
|
for _, word in heading.words_iter():
|
|
title += word.text + " "
|
|
title = title.strip()
|
|
|
|
# Add to TOC
|
|
level = heading.level.value # Get numeric value from HeadingLevel enum
|
|
toc.append((level, title, heading))
|
|
|
|
return toc
|
|
|
|
def get_or_create_style(self,
|
|
font_family: FontFamily = FontFamily.SERIF,
|
|
font_size: Union[FontSize, int] = FontSize.MEDIUM,
|
|
font_weight: FontWeight = FontWeight.NORMAL,
|
|
font_style: FontStyle = FontStyle.NORMAL,
|
|
text_decoration: TextDecoration = TextDecoration.NONE,
|
|
color: Union[str, Tuple[int, int, int]] = "black",
|
|
background_color: Optional[Union[str, Tuple[int, int, int, int]]] = None,
|
|
language: str = "en-US",
|
|
**kwargs) -> Tuple[str, AbstractStyle]:
|
|
"""
|
|
Get or create an abstract style with the specified properties.
|
|
|
|
Args:
|
|
font_family: Semantic font family
|
|
font_size: Font size (semantic or numeric)
|
|
font_weight: Font weight
|
|
font_style: Font style
|
|
text_decoration: Text decoration
|
|
color: Text color (name or RGB tuple)
|
|
background_color: Background color
|
|
language: Language code
|
|
**kwargs: Additional style properties
|
|
|
|
Returns:
|
|
Tuple of (style_id, AbstractStyle)
|
|
"""
|
|
abstract_style = AbstractStyle(
|
|
font_family=font_family,
|
|
font_size=font_size,
|
|
font_weight=font_weight,
|
|
font_style=font_style,
|
|
text_decoration=text_decoration,
|
|
color=color,
|
|
background_color=background_color,
|
|
language=language,
|
|
**kwargs
|
|
)
|
|
|
|
return self._abstract_style_registry.get_or_create_style(abstract_style)
|
|
|
|
def get_font_for_style(self, abstract_style: AbstractStyle) -> Font:
|
|
"""
|
|
Get a Font object for an AbstractStyle (for rendering).
|
|
|
|
Args:
|
|
abstract_style: The abstract style to get a font for
|
|
|
|
Returns:
|
|
Font object ready for rendering
|
|
"""
|
|
return self._concrete_style_registry.get_font(abstract_style)
|
|
|
|
def update_rendering_context(self, **kwargs):
|
|
"""
|
|
Update the rendering context (user preferences, device settings, etc.).
|
|
|
|
Args:
|
|
**kwargs: Context properties to update (base_font_size, font_scale_factor, etc.)
|
|
"""
|
|
self._style_resolver.update_context(**kwargs)
|
|
|
|
def get_style_registry(self) -> AbstractStyleRegistry:
|
|
"""Get the abstract style registry for this document."""
|
|
return self._abstract_style_registry
|
|
|
|
def get_concrete_style_registry(self) -> ConcreteStyleRegistry:
|
|
"""Get the concrete style registry for this document."""
|
|
return self._concrete_style_registry
|
|
|
|
def get_or_create_font(self,
|
|
font_path: Optional[str] = None,
|
|
font_size: int = 16,
|
|
colour: Tuple[int, int, int] = (0, 0, 0),
|
|
weight: FontWeight = FontWeight.NORMAL,
|
|
style: FontStyle = FontStyle.NORMAL,
|
|
decoration: TextDecoration = TextDecoration.NONE,
|
|
background: Optional[Tuple[int, int, int, int]] = None,
|
|
language: str = "en_EN",
|
|
min_hyphenation_width: Optional[int] = None) -> Font:
|
|
"""
|
|
Get or create a font with the specified properties.
|
|
|
|
Args:
|
|
font_path: Path to the font file (.ttf, .otf). If None, uses default font.
|
|
font_size: Size of the font in points.
|
|
colour: RGB color tuple for the text.
|
|
weight: Font weight (normal or bold).
|
|
style: Font style (normal or italic).
|
|
decoration: Text decoration (none, underline, or strikethrough).
|
|
background: RGBA background color for the text. If None, transparent background.
|
|
language: Language code for hyphenation and text processing.
|
|
min_hyphenation_width: Minimum width in pixels required for hyphenation.
|
|
|
|
Returns:
|
|
Font object (either existing or newly created)
|
|
"""
|
|
# Initialize font registry if it doesn't exist
|
|
if not hasattr(self, '_fonts'):
|
|
self._fonts: Dict[str, Font] = {}
|
|
|
|
# Create a unique key for this font configuration
|
|
bg_tuple = background if background else (255, 255, 255, 0)
|
|
min_hyph_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4
|
|
|
|
font_key = (
|
|
font_path,
|
|
font_size,
|
|
colour,
|
|
weight.value if isinstance(weight, FontWeight) else weight,
|
|
style.value if isinstance(style, FontStyle) else style,
|
|
decoration.value if isinstance(decoration, TextDecoration) else decoration,
|
|
bg_tuple,
|
|
language,
|
|
min_hyph_width
|
|
)
|
|
|
|
# Convert tuple to string for dictionary key
|
|
key_str = str(font_key)
|
|
|
|
# Check if we already have this font
|
|
if key_str in self._fonts:
|
|
return self._fonts[key_str]
|
|
|
|
# Create new font and store it
|
|
new_font = Font(
|
|
font_path=font_path,
|
|
font_size=font_size,
|
|
colour=colour,
|
|
weight=weight,
|
|
style=style,
|
|
decoration=decoration,
|
|
background=background,
|
|
language=language,
|
|
min_hyphenation_width=min_hyphenation_width
|
|
)
|
|
|
|
self._fonts[key_str] = new_font
|
|
return new_font
|
|
|
|
|
|
class Chapter:
|
|
"""
|
|
Represents a chapter or section in a document.
|
|
A chapter contains a sequence of blocks and has metadata.
|
|
"""
|
|
|
|
def __init__(self, title: Optional[str] = None, level: int = 1, style=None, parent=None):
|
|
"""
|
|
Initialize a new chapter.
|
|
|
|
Args:
|
|
title: The chapter title
|
|
level: The chapter level (1 = top level, 2 = subsection, etc.)
|
|
style: Optional default style for child blocks
|
|
parent: Parent container (e.g., Document or Book)
|
|
"""
|
|
self._title = title
|
|
self._level = level
|
|
self._blocks: List[Block] = []
|
|
self._metadata: Dict[str, Any] = {}
|
|
self._style = style
|
|
self._parent = parent
|
|
self._fonts: Dict[str, Font] = {} # Local font registry
|
|
|
|
@property
|
|
def title(self) -> Optional[str]:
|
|
"""Get the chapter title"""
|
|
return self._title
|
|
|
|
@title.setter
|
|
def title(self, title: str):
|
|
"""Set the chapter title"""
|
|
self._title = title
|
|
|
|
@property
|
|
def level(self) -> int:
|
|
"""Get the chapter level"""
|
|
return self._level
|
|
|
|
@property
|
|
def blocks(self) -> List[Block]:
|
|
"""Get the blocks in this chapter"""
|
|
return self._blocks
|
|
|
|
@property
|
|
def style(self):
|
|
"""Get the default style for this chapter"""
|
|
return self._style
|
|
|
|
@style.setter
|
|
def style(self, style):
|
|
"""Set the default style for this chapter"""
|
|
self._style = style
|
|
|
|
def add_block(self, block: Block):
|
|
"""
|
|
Add a block to this chapter.
|
|
|
|
Args:
|
|
block: The block to add
|
|
"""
|
|
self._blocks.append(block)
|
|
|
|
def create_paragraph(self, style=None) -> Paragraph:
|
|
"""
|
|
Create a new paragraph and add it to this chapter.
|
|
|
|
Args:
|
|
style: Optional style override. If None, inherits from chapter
|
|
|
|
Returns:
|
|
The newly created Paragraph object
|
|
"""
|
|
if style is None:
|
|
style = self._style
|
|
paragraph = Paragraph(style)
|
|
self.add_block(paragraph)
|
|
return paragraph
|
|
|
|
def create_heading(self, level: HeadingLevel = HeadingLevel.H1, style=None) -> Heading:
|
|
"""
|
|
Create a new heading and add it to this chapter.
|
|
|
|
Args:
|
|
level: The heading level
|
|
style: Optional style override. If None, inherits from chapter
|
|
|
|
Returns:
|
|
The newly created Heading object
|
|
"""
|
|
if style is None:
|
|
style = self._style
|
|
heading = Heading(level, style)
|
|
self.add_block(heading)
|
|
return heading
|
|
|
|
def set_metadata(self, key: str, value: Any):
|
|
"""
|
|
Set a metadata value.
|
|
|
|
Args:
|
|
key: The metadata key
|
|
value: The metadata value
|
|
"""
|
|
self._metadata[key] = value
|
|
|
|
def get_metadata(self, key: str) -> Optional[Any]:
|
|
"""
|
|
Get a metadata value.
|
|
|
|
Args:
|
|
key: The metadata key
|
|
|
|
Returns:
|
|
The metadata value, or None if not set
|
|
"""
|
|
return self._metadata.get(key)
|
|
|
|
def get_or_create_font(self,
|
|
font_path: Optional[str] = None,
|
|
font_size: int = 16,
|
|
colour: Tuple[int, int, int] = (0, 0, 0),
|
|
weight: FontWeight = FontWeight.NORMAL,
|
|
style: FontStyle = FontStyle.NORMAL,
|
|
decoration: TextDecoration = TextDecoration.NONE,
|
|
background: Optional[Tuple[int, int, int, int]] = None,
|
|
language: str = "en_EN",
|
|
min_hyphenation_width: Optional[int] = None) -> Font:
|
|
"""
|
|
Get or create a font with the specified properties. Cascades to parent if available.
|
|
|
|
Args:
|
|
font_path: Path to the font file (.ttf, .otf). If None, uses default font.
|
|
font_size: Size of the font in points.
|
|
colour: RGB color tuple for the text.
|
|
weight: Font weight (normal or bold).
|
|
style: Font style (normal or italic).
|
|
decoration: Text decoration (none, underline, or strikethrough).
|
|
background: RGBA background color for the text. If None, transparent background.
|
|
language: Language code for hyphenation and text processing.
|
|
min_hyphenation_width: Minimum width in pixels required for hyphenation.
|
|
|
|
Returns:
|
|
Font object (either existing or newly created)
|
|
"""
|
|
# If we have a parent with font management, delegate to parent
|
|
if self._parent and hasattr(self._parent, 'get_or_create_font'):
|
|
return self._parent.get_or_create_font(
|
|
font_path=font_path,
|
|
font_size=font_size,
|
|
colour=colour,
|
|
weight=weight,
|
|
style=style,
|
|
decoration=decoration,
|
|
background=background,
|
|
language=language,
|
|
min_hyphenation_width=min_hyphenation_width
|
|
)
|
|
|
|
# Otherwise manage our own fonts
|
|
# Create a unique key for this font configuration
|
|
bg_tuple = background if background else (255, 255, 255, 0)
|
|
min_hyph_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4
|
|
|
|
font_key = (
|
|
font_path,
|
|
font_size,
|
|
colour,
|
|
weight.value if isinstance(weight, FontWeight) else weight,
|
|
style.value if isinstance(style, FontStyle) else style,
|
|
decoration.value if isinstance(decoration, TextDecoration) else decoration,
|
|
bg_tuple,
|
|
language,
|
|
min_hyph_width
|
|
)
|
|
|
|
# Convert tuple to string for dictionary key
|
|
key_str = str(font_key)
|
|
|
|
# Check if we already have this font
|
|
if key_str in self._fonts:
|
|
return self._fonts[key_str]
|
|
|
|
# Create new font and store it
|
|
new_font = Font(
|
|
font_path=font_path,
|
|
font_size=font_size,
|
|
colour=colour,
|
|
weight=weight,
|
|
style=style,
|
|
decoration=decoration,
|
|
background=background,
|
|
language=language,
|
|
min_hyphenation_width=min_hyphenation_width
|
|
)
|
|
|
|
self._fonts[key_str] = new_font
|
|
return new_font
|
|
|
|
|
|
class Book(Document):
|
|
"""
|
|
Abstract representation of an ebook.
|
|
A book is a document that contains chapters.
|
|
"""
|
|
|
|
def __init__(self, title: Optional[str] = None, author: Optional[str] = None,
|
|
language: str = "en-US", default_style=None):
|
|
"""
|
|
Initialize a new book.
|
|
|
|
Args:
|
|
title: The book title
|
|
author: The book author
|
|
language: The book language code
|
|
default_style: Optional default style for child chapters and blocks
|
|
"""
|
|
super().__init__(title, language, default_style)
|
|
self._chapters: List[Chapter] = []
|
|
|
|
if author:
|
|
self.set_metadata(MetadataType.AUTHOR, author)
|
|
|
|
@property
|
|
def chapters(self) -> List[Chapter]:
|
|
"""Get the chapters in this book"""
|
|
return self._chapters
|
|
|
|
def add_chapter(self, chapter: Chapter):
|
|
"""
|
|
Add a chapter to this book.
|
|
|
|
Args:
|
|
chapter: The chapter to add
|
|
"""
|
|
self._chapters.append(chapter)
|
|
|
|
def create_chapter(self, title: Optional[str] = None, level: int = 1, style=None) -> Chapter:
|
|
"""
|
|
Create and add a new chapter with inherited style.
|
|
|
|
Args:
|
|
title: The chapter title
|
|
level: The chapter level
|
|
style: Optional style override. If None, inherits from book
|
|
|
|
Returns:
|
|
The new chapter
|
|
"""
|
|
if style is None:
|
|
style = self._default_style
|
|
chapter = Chapter(title, level, style)
|
|
self.add_chapter(chapter)
|
|
return chapter
|
|
|
|
def get_author(self) -> Optional[str]:
|
|
"""
|
|
Get the book author.
|
|
|
|
Returns:
|
|
The book author, or None if not set
|
|
"""
|
|
return self.get_metadata(MetadataType.AUTHOR)
|
|
|
|
def set_author(self, author: str):
|
|
"""
|
|
Set the book author.
|
|
|
|
Args:
|
|
author: The book author
|
|
"""
|
|
self.set_metadata(MetadataType.AUTHOR, author)
|
|
|
|
def generate_table_of_contents(self) -> List[Tuple[int, str, Chapter]]:
|
|
"""
|
|
Generate a table of contents from chapters.
|
|
|
|
Returns:
|
|
A list of tuples containing (level, title, chapter)
|
|
"""
|
|
toc = []
|
|
for chapter in self._chapters:
|
|
if chapter.title:
|
|
toc.append((chapter.level, chapter.title, chapter))
|
|
|
|
return toc
|