|
1 | 1 | from markdown import markdown |
2 | | -from bs4 import BeautifulSoup |
| 2 | +from bs4 import BeautifulSoup, Tag |
| 3 | +import re |
3 | 4 |
|
4 | | -def render_minutes_with_tailwind(md_text): |
5 | | - html = markdown(md_text, extensions=['fenced_code', 'codehilite', 'tables', 'nl2br']) |
| 5 | +TAG_STYLES = { |
| 6 | + 'h1': 'text-4xl font-extrabold text-indigo-800 mb-6 mt-8 scroll-mt-24 flex justify-between items-center', |
| 7 | + 'h2': 'text-3xl font-bold text-indigo-700 mb-5 mt-7 scroll-mt-24 flex justify-between items-center', |
| 8 | + 'h3': 'text-2xl font-semibold text-indigo-600 mb-4 mt-6 flex justify-between items-center', |
| 9 | + 'h4': 'text-xl font-medium text-indigo-500 mb-3 mt-5', |
| 10 | + 'h5': 'text-lg font-medium text-indigo-400 mb-2 mt-4', |
| 11 | + 'h6': 'text-base font-medium text-indigo-300 mb-1 mt-3', |
| 12 | + 'p': 'mb-4 text-gray-800 leading-relaxed tracking-normal', |
| 13 | + 'ul': 'list-disc list-inside pl-6 mb-4 text-gray-800', |
| 14 | + 'ol': 'list-decimal list-inside pl-6 mb-4 text-gray-800', |
| 15 | + 'li': 'mb-1', |
| 16 | + 'blockquote': 'border-l-4 border-blue-400 pl-6 italic text-gray-700 bg-blue-50 py-3 px-4 rounded-md my-6', |
| 17 | + 'hr': 'my-8 border-t border-gray-300', |
| 18 | + 'a': 'text-blue-700 hover:text-blue-900 underline', |
| 19 | + 'code': 'bg-gray-100 px-1.5 py-0.5 rounded text-sm font-mono text-purple-700', |
| 20 | + 'pre': 'bg-gray-100 text-gray-800 p-5 rounded-lg overflow-x-auto text-sm shadow-inner my-6 whitespace-pre-wrap break-words', |
| 21 | + 'table': 'table-auto w-full border-collapse border border-gray-300 shadow-sm my-8 text-sm', |
| 22 | + 'thead': 'bg-gray-100', |
| 23 | + 'th': 'border px-4 py-3 text-left bg-gray-200 text-gray-700 font-semibold', |
| 24 | + 'td': 'border px-4 py-2 text-gray-800' |
| 25 | +} |
| 26 | + |
| 27 | +HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] |
| 28 | + |
| 29 | +def wrap_collapsible(start_tag, soup): |
| 30 | + """ |
| 31 | + Wraps content under heading inside a collapsible div with toggle icon |
| 32 | + """ |
| 33 | + level = int(start_tag.name[1]) |
| 34 | + toggle_id = f"section-{id(start_tag)}" |
| 35 | + content_div = soup.new_tag("div", **{'id': toggle_id, 'class': 'collapsible-content'}) |
| 36 | + |
| 37 | + next_sibling = start_tag.find_next_sibling() |
| 38 | + while next_sibling and ( |
| 39 | + not isinstance(next_sibling, Tag) |
| 40 | + or next_sibling.name not in HEADING_TAGS |
| 41 | + or int(next_sibling.name[1]) > level |
| 42 | + ): |
| 43 | + temp = next_sibling |
| 44 | + next_sibling = next_sibling.find_next_sibling() |
| 45 | + content_div.append(temp.extract()) |
| 46 | + |
| 47 | + start_tag.insert_after(content_div) |
| 48 | + |
| 49 | + # Add toggle button |
| 50 | + toggle_button = soup.new_tag("button", **{ |
| 51 | + 'class': 'toggle-button text-indigo-700 text-xl font-bold ml-2 focus:outline-none', |
| 52 | + 'onclick': f"toggleContent('{toggle_id}', this)" |
| 53 | + }) |
| 54 | + toggle_button.string = '+' |
| 55 | + start_tag.append(toggle_button) |
| 56 | + |
| 57 | +def fix_nested_lists(soup): |
| 58 | + for ul in soup.find_all(['ul', 'ol']): |
| 59 | + for li in ul.find_all('li', recursive=False): |
| 60 | + next_elem = li.find_next_sibling() |
| 61 | + if next_elem and next_elem.name in ['ul', 'ol']: |
| 62 | + nested_list = next_elem.extract() |
| 63 | + li.append(nested_list) |
| 64 | + |
| 65 | +def process_nested_items(soup): |
| 66 | + for li in soup.find_all('li'): |
| 67 | + if li.strong and li.strong.text.strip().endswith(':'): |
| 68 | + next_sibling = li.find_next_sibling() |
| 69 | + sublist = soup.new_tag('ul', **{'class': TAG_STYLES['ul'].split()}) |
| 70 | + while next_sibling and next_sibling.name == 'li' and not next_sibling.strong: |
| 71 | + temp = next_sibling |
| 72 | + next_sibling = next_sibling.find_next_sibling() |
| 73 | + sublist.append(temp.extract()) |
| 74 | + if sublist.find('li'): |
| 75 | + li.append(sublist) |
| 76 | + |
| 77 | +def handle_special_formatting(soup): |
| 78 | + for code in soup.find_all('code'): |
| 79 | + if code.parent.name != 'pre': |
| 80 | + code['class'] = TAG_STYLES['code'].split() |
| 81 | + # Leave checkbox syntax unchanged |
| 82 | + |
| 83 | +def render_minutes_with_tailwind(md_text: str) -> str: |
| 84 | + html = markdown(md_text, extensions=['fenced_code', 'codehilite', 'tables', 'nl2br', 'extra']) |
6 | 85 | soup = BeautifulSoup(html, 'html.parser') |
7 | 86 |
|
8 | | - # Headings – updated for white background |
9 | | - heading_styles = { |
10 | | - 'h1': 'text-4xl font-extrabold text-indigo-800 mb-6 mt-8', |
11 | | - 'h2': 'text-3xl font-bold text-indigo-700 mb-5 mt-7', |
12 | | - 'h3': 'text-2xl font-semibold text-indigo-600 mb-4 mt-6', |
13 | | - 'h4': 'text-xl font-medium text-indigo-500 mb-3 mt-5', |
14 | | - 'h5': 'text-lg font-medium text-indigo-400 mb-2 mt-4', |
15 | | - 'h6': 'text-base font-medium text-indigo-300 mb-1 mt-3', |
16 | | - } |
17 | | - for tag, classes in heading_styles.items(): |
18 | | - for el in soup.find_all(tag): |
19 | | - el['class'] = classes + ' scroll-mt-24' # type: ignore |
20 | | - |
21 | | - # Paragraphs |
22 | | - for tag in soup.find_all('p'): |
23 | | - tag['class'] = 'mb-4 text-gray-800 leading-relaxed tracking-normal' # type: ignore |
24 | | - |
25 | | - # Lists |
26 | | - for tag in soup.find_all('ul'): |
27 | | - tag['class'] = 'list-disc list-inside pl-6 mb-4 text-gray-800' # type: ignore |
28 | | - for tag in soup.find_all('ol'): |
29 | | - tag['class'] = 'list-decimal list-inside pl-6 mb-4 text-gray-800' # type: ignore |
30 | | - for tag in soup.find_all('li'): |
31 | | - tag['class'] = 'mb-1' # type: ignore |
32 | | - |
33 | | - # Blockquotes |
34 | | - for tag in soup.find_all('blockquote'): |
35 | | - tag['class'] = ( |
36 | | - 'border-l-4 border-blue-400 pl-6 italic text-gray-700 ' |
37 | | - 'bg-blue-50 py-3 px-4 rounded-md my-6' |
38 | | - ) # type: ignore |
39 | | - |
40 | | - # Horizontal Rules |
41 | | - for tag in soup.find_all('hr'): |
42 | | - tag['class'] = 'my-8 border-t border-gray-300' # type: ignore |
43 | | - |
44 | | - # Links |
45 | | - for tag in soup.find_all('a'): |
46 | | - tag['class'] = 'text-blue-700 hover:text-blue-900 underline' # type: ignore |
47 | | - tag['target'] = '_blank' # type: ignore |
48 | | - tag['rel'] = 'noopener noreferrer' # type: ignore |
49 | | - |
50 | | - # Inline Code |
51 | | - for tag in soup.find_all('code'): |
52 | | - if tag.parent.name != 'pre': # type: ignore |
53 | | - tag['class'] = 'bg-gray-100 px-1.5 py-0.5 rounded text-sm font-mono text-purple-700' # type: ignore |
54 | | - |
55 | | - # Code Blocks |
56 | | - for tag in soup.find_all('pre'): |
57 | | - tag['class'] = ( |
58 | | - 'bg-gray-100 text-gray-800 p-5 rounded-lg overflow-x-auto text-sm ' |
59 | | - 'shadow-inner my-6 whitespace-pre-wrap break-words' |
60 | | - ) # type: ignore |
61 | | - |
62 | | - # Tables |
63 | | - for tag in soup.find_all('table'): |
64 | | - tag['class'] = 'table-auto w-full border-collapse border border-gray-300 shadow-sm my-8 text-sm' # type: ignore |
65 | | - for tag in soup.find_all('thead'): |
66 | | - tag['class'] = 'bg-gray-100' # type: ignore |
67 | | - for tag in soup.find_all('th'): |
68 | | - tag['class'] = 'border px-4 py-3 text-left bg-gray-200 text-gray-700 font-semibold' # type: ignore |
69 | | - for tag in soup.find_all('td'): |
70 | | - tag['class'] = 'border px-4 py-2 text-gray-800' # type: ignore |
| 87 | + fix_nested_lists(soup) |
| 88 | + process_nested_items(soup) |
| 89 | + handle_special_formatting(soup) |
| 90 | + |
| 91 | + for tag in soup.find_all(HEADING_TAGS): |
| 92 | + level = int(tag.name[1]) |
| 93 | + if level <= 3: |
| 94 | + wrap_collapsible(tag, soup) |
| 95 | + |
| 96 | + for tag_name, class_list in TAG_STYLES.items(): |
| 97 | + for el in soup.find_all(tag_name): |
| 98 | + if tag_name == 'code' and el.parent.name == 'pre': |
| 99 | + continue |
| 100 | + existing_classes = el.get('class', []) |
| 101 | + el['class'] = list(set(existing_classes + class_list.split())) |
| 102 | + if tag_name == 'a': |
| 103 | + el['target'] = '_blank' |
| 104 | + el['rel'] = 'noopener noreferrer' |
71 | 105 |
|
72 | 106 | return str(soup) |
| 107 | + |
| 108 | +if __name__ == "__main__": |
| 109 | + with open("data.md", "r") as f: |
| 110 | + md_text = f.read() |
| 111 | + |
| 112 | + html = render_minutes_with_tailwind(md_text) |
| 113 | + with open("output.html", "w") as f: |
| 114 | + f.write(html) |
0 commit comments