Skip to content

Document Rendering Module

Markdown-based document rendering system using QTextBrowser with automatic theme support.

Quick Start

from src.shared_services.rendering.documents.api import MarkdownView

view = MarkdownView(parent=self, object_name="NewsView")
view.set_markdown("# Titel\n\nAbsatz mit **fettem** Text.")

MarkdownView Widget

Theme-aware QTextBrowser wrapper that renders Markdown with GitHub-style alerts.

from src.shared_services.rendering.documents.api import MarkdownView

# Render Markdown content
view = MarkdownView(parent=self)
view.set_markdown("# Hello\n\n> [!NOTE]\n> This is a note.")

# Set pre-built HTML (still gets theme stylesheet)
view.set_html_content("<h1>Titel</h1><p>Some HTML content.</p>")

Supported Markdown Features

  • CommonMark (headings, bold, italic, links, code blocks, etc.)
  • GFM tables
  • Task lists
  • Strikethrough
  • GitHub-style alert blocks: [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]

Theme Integration

MarkdownView automatically re-renders when the application theme changes. No manual theme callback registration is needed.

Fun Facts

Daily rotating facts injected at placeholder locations.

from src.shared_services.rendering.documents.fun_facts import inject_fun_fact

md = "# Welcome\n\n{{FUN_FACT}}"
md = inject_fun_fact(md)

Files

File Purpose
api.py Public API exports
markdown_view.py MarkdownView widget (QTextBrowser + markdown-it-py)
fun_facts.py Daily rotating facts

API Reference

src.shared_services.rendering.documents.api

Document rendering public API.

Provides the MarkdownView widget for displaying Markdown content with automatic theme support and GitHub-style alert blocks.

USAGE::

from src.shared_services.rendering.documents.api import MarkdownView

view = MarkdownView(parent=self)
view.set_markdown("# Willkommen\n\nEin Absatz mit **fettem** Text.")

FunFactsManager

Manager for daily rotating fun facts.

Uses date-based selection to ensure the same fact is shown throughout a single day, with caching to persist across restarts.

Source code in src\shared_services\rendering\documents\fun_facts.py
class FunFactsManager:
    """
    Manager for daily rotating fun facts.

    Uses date-based selection to ensure the same fact is shown
    throughout a single day, with caching to persist across restarts.
    """

    def __init__(self, cache_dir: Optional[str] = None) -> None:
        """
        Initialize the fun facts manager.

        Args:
            cache_dir: Directory to store the cache file.
                If None, uses the application data directory.
        """
        self._cache_dir = cache_dir
        self._cache_file: Optional[str] = None

    def _get_cache_path(self) -> str:
        """Get the path to the cache file."""
        if self._cache_file:
            return self._cache_file

        if self._cache_dir:
            cache_dir = self._cache_dir
        else:
            try:
                from src.shared_services.constants.paths import Cache
                from src.shared_services.path_management.api import get_path_str

                cache_dir = get_path_str(Cache.FunFactsDir)
            except (ImportError, Exception):
                cache_dir = os.path.join(os.path.expanduser("~"), ".gehasoftware")

        os.makedirs(cache_dir, exist_ok=True)
        self._cache_file = os.path.join(cache_dir, "fun_facts_cache.json")
        return self._cache_file

    def get_daily_fact(self) -> str:
        """
        Get the fact for today.

        Returns:
            A fun fact string, prefixed with a lightbulb indicator.

        Example::

            manager = FunFactsManager()
            fact = manager.get_daily_fact()
            # Returns something like "[*] Honey never spoils..."
        """
        cache_path = self._get_cache_path()

        # Try to load from cache
        try:
            if os.path.exists(cache_path):
                with open(cache_path, "r", encoding="utf-8") as f:
                    cache_data: Dict = json.load(f)

                cached_date = datetime.fromisoformat(cache_data["date"])
                if datetime.now() - cached_date < timedelta(days=1):
                    return cache_data['fact']
        except (json.JSONDecodeError, KeyError, ValueError):
            pass

        # Select new fact based on date
        fact = self._select_fact_for_today()

        # Save to cache
        self._save_to_cache(cache_path, fact)

        return fact

    def _select_fact_for_today(self) -> str:
        """Select a fact based on the current date."""
        today = datetime.now().date()
        day_of_year = today.timetuple().tm_yday
        year = today.year

        # Use date as seed for consistent selection on the same day
        fact_index = (day_of_year + year) % len(_INTERESTING_FACTS)
        return _INTERESTING_FACTS[fact_index]

    def _save_to_cache(self, cache_path: str, fact: str) -> None:
        """Save a fact to the cache file."""
        try:
            cache_data = {
                "fact": fact,
                "date": datetime.now().isoformat(),
            }
            with open(cache_path, "w", encoding="utf-8") as f:
                json.dump(cache_data, f, ensure_ascii=False, indent=2)
        except OSError:
            pass  # Cache write failure is not critical
__init__(cache_dir=None)

Initialize the fun facts manager.

Parameters:

Name Type Description Default
cache_dir Optional[str]

Directory to store the cache file. If None, uses the application data directory.

None
Source code in src\shared_services\rendering\documents\fun_facts.py
def __init__(self, cache_dir: Optional[str] = None) -> None:
    """
    Initialize the fun facts manager.

    Args:
        cache_dir: Directory to store the cache file.
            If None, uses the application data directory.
    """
    self._cache_dir = cache_dir
    self._cache_file: Optional[str] = None
get_daily_fact()

Get the fact for today.

Returns:

Type Description
str

A fun fact string, prefixed with a lightbulb indicator.

Example::

manager = FunFactsManager()
fact = manager.get_daily_fact()
# Returns something like "[*] Honey never spoils..."
Source code in src\shared_services\rendering\documents\fun_facts.py
def get_daily_fact(self) -> str:
    """
    Get the fact for today.

    Returns:
        A fun fact string, prefixed with a lightbulb indicator.

    Example::

        manager = FunFactsManager()
        fact = manager.get_daily_fact()
        # Returns something like "[*] Honey never spoils..."
    """
    cache_path = self._get_cache_path()

    # Try to load from cache
    try:
        if os.path.exists(cache_path):
            with open(cache_path, "r", encoding="utf-8") as f:
                cache_data: Dict = json.load(f)

            cached_date = datetime.fromisoformat(cache_data["date"])
            if datetime.now() - cached_date < timedelta(days=1):
                return cache_data['fact']
    except (json.JSONDecodeError, KeyError, ValueError):
        pass

    # Select new fact based on date
    fact = self._select_fact_for_today()

    # Save to cache
    self._save_to_cache(cache_path, fact)

    return fact

MarkdownView

Bases: QTextBrowser

Theme-aware Markdown rendering widget.

Renders Markdown content as GitHub-styled HTML inside a QTextBrowser. Supports GFM alert blocks, tables, task lists, and automatic re-rendering on theme changes.

Usage::

view = MarkdownView(parent=self)
view.set_markdown("# Hello\n\nSome **bold** text.")
Source code in src\shared_services\rendering\documents\markdown_view.py
class MarkdownView(QTextBrowser):
    """Theme-aware Markdown rendering widget.

    Renders Markdown content as GitHub-styled HTML inside a QTextBrowser.
    Supports GFM alert blocks, tables, task lists, and automatic
    re-rendering on theme changes.

    Usage::

        view = MarkdownView(parent=self)
        view.set_markdown("# Hello\\n\\nSome **bold** text.")
    """

    def __init__(
        self,
        parent: Optional[QWidget] = None,
        object_name: str = "MarkdownView",
    ) -> None:
        super().__init__(parent)
        self.setObjectName(object_name)
        self.setOpenExternalLinks(False)
        self.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
        self.setFont(QFont("Segoe UI", 10))

        self._parser = _create_parser()
        self._current_theme = self._resolve_theme()
        self._markdown_text: Optional[str] = None
        self._raw_html: Optional[str] = None
        self._theme_callback_registered = False

        self._register_theme_callback()

    def set_markdown(self, text: str) -> None:
        """Render Markdown text into the view.

        Args:
            text: Raw Markdown string.
        """
        self._markdown_text = text
        self._raw_html = None
        self._render()

    def set_html_content(self, html: str) -> None:
        """Set pre-built HTML content directly.

        Use this for programmatically generated HTML (complete documents
        or body fragments). Complete documents (containing <html> tag)
        are passed through as-is; fragments are wrapped with the
        MarkdownView theme stylesheet.

        Args:
            html: HTML string (complete document or body fragment).
        """
        self._raw_html = html
        self._markdown_text = None
        self._render()

    def set_theme(self, theme: str) -> None:
        """Force a specific theme and re-render.

        Args:
            theme: "light" or "dark".
        """
        if theme in ("light", "dark", "soft", "gray"):
            self._current_theme = theme
            self._render()

    def _render(self) -> None:
        """Render current content with the active theme."""
        theme = self._current_theme

        if self._markdown_text is not None:
            html_body = self._parser.render(self._markdown_text)
            html_body = _convert_alerts(html_body, theme)

            stylesheet = _build_stylesheet(theme)
            full_html = (
                "<!DOCTYPE html>"
                "<html><head>"
                f"<style>{stylesheet}</style>"
                "</head><body>"
                f'<div class="markdown-body">{html_body}</div>'
                "</body></html>"
            )
        elif self._raw_html is not None:
            # Complete documents are passed through as-is
            if "<html" in self._raw_html.lower():
                full_html = self._raw_html
            else:
                stylesheet = _build_stylesheet(theme)
                full_html = (
                    "<!DOCTYPE html>"
                    "<html><head>"
                    f"<style>{stylesheet}</style>"
                    "</head><body>"
                    f"{self._raw_html}"
                    "</body></html>"
                )
        else:
            return

        # Register icon resources before setting HTML
        _register_alert_icons(self.document(), theme)
        self.setHtml(full_html)

    def _on_theme_change(self, theme: str) -> None:
        """Handle application theme change."""
        self._current_theme = theme
        self._render()

    def _register_theme_callback(self) -> None:
        """Register for theme change notifications."""
        if self._theme_callback_registered:
            return
        manager = StylesheetManager.instance()
        manager.on_theme_change(self._on_theme_change)
        self._theme_callback_registered = True

    @staticmethod
    def _resolve_theme() -> str:
        """Get the current application theme."""
        try:
            manager = StylesheetManager.instance()
            return manager.get_theme()
        except Exception:
            return "light"
set_html_content(html)

Set pre-built HTML content directly.

Use this for programmatically generated HTML (complete documents or body fragments). Complete documents (containing tag) are passed through as-is; fragments are wrapped with the MarkdownView theme stylesheet.

Parameters:

Name Type Description Default
html str

HTML string (complete document or body fragment).

required
Source code in src\shared_services\rendering\documents\markdown_view.py
def set_html_content(self, html: str) -> None:
    """Set pre-built HTML content directly.

    Use this for programmatically generated HTML (complete documents
    or body fragments). Complete documents (containing <html> tag)
    are passed through as-is; fragments are wrapped with the
    MarkdownView theme stylesheet.

    Args:
        html: HTML string (complete document or body fragment).
    """
    self._raw_html = html
    self._markdown_text = None
    self._render()
set_markdown(text)

Render Markdown text into the view.

Parameters:

Name Type Description Default
text str

Raw Markdown string.

required
Source code in src\shared_services\rendering\documents\markdown_view.py
def set_markdown(self, text: str) -> None:
    """Render Markdown text into the view.

    Args:
        text: Raw Markdown string.
    """
    self._markdown_text = text
    self._raw_html = None
    self._render()
set_theme(theme)

Force a specific theme and re-render.

Parameters:

Name Type Description Default
theme str

"light" or "dark".

required
Source code in src\shared_services\rendering\documents\markdown_view.py
def set_theme(self, theme: str) -> None:
    """Force a specific theme and re-render.

    Args:
        theme: "light" or "dark".
    """
    if theme in ("light", "dark", "soft", "gray"):
        self._current_theme = theme
        self._render()

get_daily_fact()

Get today's fun fact.

Convenience function that uses the singleton manager.

Returns:

Type Description
str

A fun fact string with lightbulb prefix.

Example::

fact = get_daily_fact()
# "[*] Octopuses have three hearts and blue blood"
Source code in src\shared_services\rendering\documents\fun_facts.py
def get_daily_fact() -> str:
    """
    Get today's fun fact.

    Convenience function that uses the singleton manager.

    Returns:
        A fun fact string with lightbulb prefix.

    Example::

        fact = get_daily_fact()
        # "[*] Octopuses have three hearts and blue blood"
    """
    return get_fun_facts_manager().get_daily_fact()

get_fun_facts_manager()

Get the singleton FunFactsManager instance.

Returns:

Type Description
FunFactsManager

The shared FunFactsManager instance.

Source code in src\shared_services\rendering\documents\fun_facts.py
def get_fun_facts_manager() -> FunFactsManager:
    """
    Get the singleton FunFactsManager instance.

    Returns:
        The shared FunFactsManager instance.
    """
    global _manager
    if _manager is None:
        _manager = FunFactsManager()
    return _manager

inject_fun_fact(text)

Replace the fun fact placeholder in text with today's fact.

Parameters:

Name Type Description Default
text str

Text that may contain {{FUN_FACT}} placeholder.

required

Returns:

Type Description
str

Text with placeholder replaced, or original text if no placeholder.

Example::

text = "Here's a fact: {{FUN_FACT}}"
result = inject_fun_fact(text)
# "Here's a fact: [*] Honey never spoils..."
Source code in src\shared_services\rendering\documents\fun_facts.py
def inject_fun_fact(text: str) -> str:
    """
    Replace the fun fact placeholder in text with today's fact.

    Args:
        text: Text that may contain {{FUN_FACT}} placeholder.

    Returns:
        Text with placeholder replaced, or original text if no placeholder.

    Example::

        text = "Here's a fact: {{FUN_FACT}}"
        result = inject_fun_fact(text)
        # "Here's a fact: [*] Honey never spoils..."
    """
    if FUN_FACT_PLACEHOLDER in text:
        return text.replace(FUN_FACT_PLACEHOLDER, get_daily_fact())
    return text