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"
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