classes.c41_rich_text

  1from typing import TYPE_CHECKING
  2from html.parser import HTMLParser
  3import markdown
  4import drawBot
  5
  6# Avoid circular import issues, hide from runtime, but keep for type checking
  7if TYPE_CHECKING:
  8    from datatypes import DParagraphProps
  9
 10
 11class KRichText:
 12    """Converts Markdown text to DrawBot FormattedString with configurable styles.
 13
 14    Maps Markdown elements (headings, paragraphs, blockquotes, horizontal rules) to
 15    DParagraphProps style definitions. Supports CSS class-based style overrides via
 16    Python-Markdown's attr_list extension.
 17
 18    Example:
 19        ```python
 20        from classes import KRichText
 21        from datatypes import DParagraphProps
 22
 23        styles = {
 24            "h1": DParagraphProps(fontSize=32, leading=1.1, paragraphTopSpacing=20),
 25            "p": DParagraphProps(fontSize=14, leading=1.4, paragraphTopSpacing=4),
 26            "p.big": DParagraphProps(fontSize=18, leading=1.3),  # Override for .big class
 27        }
 28
 29        richText = KRichText(styles)
 30
 31        markdown_content = '''
 32        # Heading
 33
 34        Normal paragraph.
 35
 36        Big paragraph.
 37        {: .big }
 38        '''
 39
 40        fs = richText.compose(markdown_content)
 41        drawBot.textBox(fs, (50, 50, 500, 700))
 42        ```
 43
 44    Supported Markdown elements:
 45        - `# h1`, `## h2`, `### h3` - Headings
 46        - Paragraphs (`<p>`)
 47        - `> blockquote`
 48        - `---` horizontal rule (rendered as vertical spacing)
 49
 50    Class-based overrides:
 51        Use `{: .classname }` after any block element to apply a class-specific style.
 52        Style dict key format: `"tag.classname"` (e.g., `"p.big"`, `"h2.subtitle"`)
 53    """
 54
 55    def __init__(self, styles: dict[str, "DParagraphProps"]):
 56        """Initialize with a mapping of HTML tags to paragraph style definitions.
 57
 58        Args:
 59            styles: Dictionary mapping tag names (optionally with .class suffix) to
 60                    DParagraphProps instances. Examples: "h1", "p", "p.big", "blockquote"
 61        """
 62        self.styles = styles
 63
 64    def compose(self, text: str) -> drawBot.FormattedString:
 65        """Convert Markdown text to a styled FormattedString.
 66
 67        Args:
 68            text: Markdown-formatted text string
 69
 70        Returns:
 71            DrawBot FormattedString with styles applied per element
 72        """
 73        # Convert Markdown to HTML with attr_list extension for class support
 74        html = markdown.markdown(text, extensions=["attr_list"])
 75
 76        # Create FormattedString and parse HTML into it
 77        fStr = drawBot.FormattedString()
 78        parser = self._Parser(self.styles, fStr)
 79        parser.feed(html)
 80
 81        return fStr
 82
 83    class _Parser(HTMLParser):
 84        """Internal HTML parser that populates a FormattedString with styled runs."""
 85
 86        def __init__(
 87            self,
 88            styles: dict[str, "DParagraphProps"],
 89            fStr: drawBot.FormattedString,
 90        ):
 91            super().__init__()
 92            self.styles = styles
 93            self.fStr = fStr
 94            self.current_tag = None
 95            self.current_class = None
 96
 97        def handle_starttag(self, tag: str, attrs: list[tuple[str, str]]):
 98            """Process opening tags and apply paragraph-level formatting."""
 99            self.current_tag = tag
100
101            # Extract class attribute for style lookup
102            attrs_dict = dict(attrs)
103            self.current_class = attrs_dict.get("class", None)
104
105            # Look up style: try "tag.class" first, fall back to "tag"
106            style = self._get_style(tag, self.current_class)
107
108            if not style:
109                return
110
111            # Handle <hr> as self-closing element (spacing only, no visible content)
112            if tag == "hr":
113                self.fStr.append("\n")
114                return
115
116            # Apply paragraph-level properties via FormattedString methods
117            # These affect the next append() call
118            self.fStr.paragraphTopSpacing(style.paragraphTopSpacing)
119            self.fStr.paragraphBottomSpacing(style.paragraphBottomSpacing)
120            self.fStr.indent(style.indent)
121            self.fStr.tailIndent(style.tailIndent)
122
123        def handle_data(self, data: str):
124            """Append text content with the current tag's style."""
125            if not data.strip():
126                return
127
128            style = self._get_style(self.current_tag, self.current_class)
129
130            if style:
131                self.fStr.append(data, **style.calc())
132            else:
133                # Unstyled fallback (should rarely happen)
134                self.fStr.append(data)
135
136        def handle_endtag(self, tag: str):
137            """Add line breaks after block elements."""
138            # Block elements need newlines to terminate paragraphs
139            block_elements = ("h1", "h2", "h3", "h4", "h5", "h6", "p", "blockquote")
140            if tag in block_elements:
141                self.fStr.append("\n")
142
143            self.current_tag = None
144            self.current_class = None
145
146        def _get_style(
147            self, tag: str, class_name: str = None
148        ) -> "DParagraphProps | None":
149            """Look up style with class fallback logic.
150
151            Priority: "tag.class" > "tag" > None
152            """
153            if tag is None:
154                return None
155
156            # Try class-specific style first
157            if class_name:
158                class_key = f"{tag}.{class_name}"
159                if class_key in self.styles:
160                    return self.styles[class_key]
161
162            # Fall back to base tag style
163            return self.styles.get(tag, None)
class KRichText:
 12class KRichText:
 13    """Converts Markdown text to DrawBot FormattedString with configurable styles.
 14
 15    Maps Markdown elements (headings, paragraphs, blockquotes, horizontal rules) to
 16    DParagraphProps style definitions. Supports CSS class-based style overrides via
 17    Python-Markdown's attr_list extension.
 18
 19    Example:
 20        ```python
 21        from classes import KRichText
 22        from datatypes import DParagraphProps
 23
 24        styles = {
 25            "h1": DParagraphProps(fontSize=32, leading=1.1, paragraphTopSpacing=20),
 26            "p": DParagraphProps(fontSize=14, leading=1.4, paragraphTopSpacing=4),
 27            "p.big": DParagraphProps(fontSize=18, leading=1.3),  # Override for .big class
 28        }
 29
 30        richText = KRichText(styles)
 31
 32        markdown_content = '''
 33        # Heading
 34
 35        Normal paragraph.
 36
 37        Big paragraph.
 38        {: .big }
 39        '''
 40
 41        fs = richText.compose(markdown_content)
 42        drawBot.textBox(fs, (50, 50, 500, 700))
 43        ```
 44
 45    Supported Markdown elements:
 46        - `# h1`, `## h2`, `### h3` - Headings
 47        - Paragraphs (`<p>`)
 48        - `> blockquote`
 49        - `---` horizontal rule (rendered as vertical spacing)
 50
 51    Class-based overrides:
 52        Use `{: .classname }` after any block element to apply a class-specific style.
 53        Style dict key format: `"tag.classname"` (e.g., `"p.big"`, `"h2.subtitle"`)
 54    """
 55
 56    def __init__(self, styles: dict[str, "DParagraphProps"]):
 57        """Initialize with a mapping of HTML tags to paragraph style definitions.
 58
 59        Args:
 60            styles: Dictionary mapping tag names (optionally with .class suffix) to
 61                    DParagraphProps instances. Examples: "h1", "p", "p.big", "blockquote"
 62        """
 63        self.styles = styles
 64
 65    def compose(self, text: str) -> drawBot.FormattedString:
 66        """Convert Markdown text to a styled FormattedString.
 67
 68        Args:
 69            text: Markdown-formatted text string
 70
 71        Returns:
 72            DrawBot FormattedString with styles applied per element
 73        """
 74        # Convert Markdown to HTML with attr_list extension for class support
 75        html = markdown.markdown(text, extensions=["attr_list"])
 76
 77        # Create FormattedString and parse HTML into it
 78        fStr = drawBot.FormattedString()
 79        parser = self._Parser(self.styles, fStr)
 80        parser.feed(html)
 81
 82        return fStr
 83
 84    class _Parser(HTMLParser):
 85        """Internal HTML parser that populates a FormattedString with styled runs."""
 86
 87        def __init__(
 88            self,
 89            styles: dict[str, "DParagraphProps"],
 90            fStr: drawBot.FormattedString,
 91        ):
 92            super().__init__()
 93            self.styles = styles
 94            self.fStr = fStr
 95            self.current_tag = None
 96            self.current_class = None
 97
 98        def handle_starttag(self, tag: str, attrs: list[tuple[str, str]]):
 99            """Process opening tags and apply paragraph-level formatting."""
100            self.current_tag = tag
101
102            # Extract class attribute for style lookup
103            attrs_dict = dict(attrs)
104            self.current_class = attrs_dict.get("class", None)
105
106            # Look up style: try "tag.class" first, fall back to "tag"
107            style = self._get_style(tag, self.current_class)
108
109            if not style:
110                return
111
112            # Handle <hr> as self-closing element (spacing only, no visible content)
113            if tag == "hr":
114                self.fStr.append("\n")
115                return
116
117            # Apply paragraph-level properties via FormattedString methods
118            # These affect the next append() call
119            self.fStr.paragraphTopSpacing(style.paragraphTopSpacing)
120            self.fStr.paragraphBottomSpacing(style.paragraphBottomSpacing)
121            self.fStr.indent(style.indent)
122            self.fStr.tailIndent(style.tailIndent)
123
124        def handle_data(self, data: str):
125            """Append text content with the current tag's style."""
126            if not data.strip():
127                return
128
129            style = self._get_style(self.current_tag, self.current_class)
130
131            if style:
132                self.fStr.append(data, **style.calc())
133            else:
134                # Unstyled fallback (should rarely happen)
135                self.fStr.append(data)
136
137        def handle_endtag(self, tag: str):
138            """Add line breaks after block elements."""
139            # Block elements need newlines to terminate paragraphs
140            block_elements = ("h1", "h2", "h3", "h4", "h5", "h6", "p", "blockquote")
141            if tag in block_elements:
142                self.fStr.append("\n")
143
144            self.current_tag = None
145            self.current_class = None
146
147        def _get_style(
148            self, tag: str, class_name: str = None
149        ) -> "DParagraphProps | None":
150            """Look up style with class fallback logic.
151
152            Priority: "tag.class" > "tag" > None
153            """
154            if tag is None:
155                return None
156
157            # Try class-specific style first
158            if class_name:
159                class_key = f"{tag}.{class_name}"
160                if class_key in self.styles:
161                    return self.styles[class_key]
162
163            # Fall back to base tag style
164            return self.styles.get(tag, None)

Converts Markdown text to DrawBot FormattedString with configurable styles.

Maps Markdown elements (headings, paragraphs, blockquotes, horizontal rules) to DParagraphProps style definitions. Supports CSS class-based style overrides via Python-Markdown's attr_list extension.

Example:
from classes import KRichText
from datatypes import DParagraphProps

styles = {
    "h1": DParagraphProps(fontSize=32, leading=1.1, paragraphTopSpacing=20),
    "p": DParagraphProps(fontSize=14, leading=1.4, paragraphTopSpacing=4),
    "p.big": DParagraphProps(fontSize=18, leading=1.3),  # Override for .big class
}

richText = KRichText(styles)

markdown_content = '''
# Heading

Normal paragraph.

Big paragraph.
{: .big }
'''

fs = richText.compose(markdown_content)
drawBot.textBox(fs, (50, 50, 500, 700))
Supported Markdown elements:
  • # h1, ## h2, ### h3 - Headings
  • Paragraphs (<p>)
  • > blockquote
  • --- horizontal rule (rendered as vertical spacing)

Class-based overrides: Use {: .classname } after any block element to apply a class-specific style. Style dict key format: "tag.classname" (e.g., "p.big", "h2.subtitle")

KRichText(styles: dict[str, datatypes.data_paragraphprops.DParagraphProps])
56    def __init__(self, styles: dict[str, "DParagraphProps"]):
57        """Initialize with a mapping of HTML tags to paragraph style definitions.
58
59        Args:
60            styles: Dictionary mapping tag names (optionally with .class suffix) to
61                    DParagraphProps instances. Examples: "h1", "p", "p.big", "blockquote"
62        """
63        self.styles = styles

Initialize with a mapping of HTML tags to paragraph style definitions.

Arguments:
  • styles: Dictionary mapping tag names (optionally with .class suffix) to DParagraphProps instances. Examples: "h1", "p", "p.big", "blockquote"
styles
def compose(self, text: str) -> drawBot.context.baseContext.FormattedString:
65    def compose(self, text: str) -> drawBot.FormattedString:
66        """Convert Markdown text to a styled FormattedString.
67
68        Args:
69            text: Markdown-formatted text string
70
71        Returns:
72            DrawBot FormattedString with styles applied per element
73        """
74        # Convert Markdown to HTML with attr_list extension for class support
75        html = markdown.markdown(text, extensions=["attr_list"])
76
77        # Create FormattedString and parse HTML into it
78        fStr = drawBot.FormattedString()
79        parser = self._Parser(self.styles, fStr)
80        parser.feed(html)
81
82        return fStr

Convert Markdown text to a styled FormattedString.

Arguments:
  • text: Markdown-formatted text string
Returns:

DrawBot FormattedString with styles applied per element