fetch_tailwind.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. #!/usr/bin/env python3
  2. import re
  3. from dataclasses import dataclass, field
  4. from pathlib import Path
  5. import requests
  6. from bs4 import BeautifulSoup
  7. @dataclass
  8. class Property:
  9. title: str
  10. description: str
  11. members: list[str]
  12. short_members: list[str] = field(init=False)
  13. common_prefix: str = field(init=False)
  14. def __post_init__(self) -> None:
  15. words = [s.split('-') for s in self.members]
  16. prefix = words[0]
  17. for w in words:
  18. i = 0
  19. while i < len(prefix) and i < len(w) and prefix[i] == w[i]:
  20. i += 1
  21. prefix = prefix[:i]
  22. if not prefix:
  23. break
  24. self.short_members = ['-'.join(word[len(prefix):]) for word in words]
  25. self.common_prefix = '-'.join(prefix) + '-' if prefix else ''
  26. if len(self.short_members) == 1:
  27. if self.title == 'Container':
  28. self.members.clear()
  29. self.short_members.clear()
  30. self.common_prefix = 'container'
  31. elif self.title in {'List Style Image', 'Content', 'Appearance'}:
  32. self.short_members = ['none']
  33. self.common_prefix = self.members[0].removesuffix('-none')
  34. else:
  35. raise ValueError(f'Unknown single-value property "{self.title}"')
  36. @property
  37. def pascal_title(self) -> str:
  38. return ''.join(word.capitalize() for word in re.sub(r'[-/ &]', ' ', self.title).split())
  39. @property
  40. def snake_title(self) -> str:
  41. return '_'.join(word.lower() for word in re.sub(r'[-/ &]', ' ', self.title).split())
  42. properties: list[Property] = []
  43. def get_soup(url: str) -> BeautifulSoup:
  44. path = Path('/tmp') / url.split('/')[-1]
  45. if path.exists():
  46. html = path.read_text()
  47. else:
  48. req = requests.get(url)
  49. html = req.text
  50. path.write_text(html)
  51. return BeautifulSoup(html, 'html.parser')
  52. soup = get_soup('https://tailwindcss.com/docs')
  53. for li in soup.select('li[class="mt-12 lg:mt-8"]'):
  54. title = li.select_one('h5').text
  55. links = li.select('li a')
  56. if title in {'Getting Started', 'Core Concepts', 'Customization', 'Base Styles', 'Official Plugins'}:
  57. continue
  58. print(f'{title}:')
  59. for a in links:
  60. soup = get_soup(f'https://tailwindcss.com{a["href"]}')
  61. title = soup.select_one('#header h1').text
  62. description = soup.select_one('#header .mt-2').text
  63. members = soup.select('.mt-10 td[class*=text-sky-400]')
  64. properties.append(Property(title, description, [p.text.split(' ')[0] for p in members]))
  65. print(f'\t{title} ({len(members)})')
  66. for file in (Path(__file__).parent / 'nicegui' / 'tailwind_types').glob('*.py'):
  67. file.unlink()
  68. for property in properties:
  69. if not property.members:
  70. continue
  71. with open(Path(__file__).parent / 'nicegui' / 'tailwind_types' / f'{property.snake_title}.py', 'w') as f:
  72. f.write('from typing_extensions import Literal\n')
  73. f.write('\n')
  74. f.write(f'{property.pascal_title} = Literal[\n')
  75. for short_member in property.short_members:
  76. f.write(f" '{short_member}',\n")
  77. f.write(']\n')
  78. with open(Path(__file__).parent / 'nicegui' / 'tailwind.py', 'w') as f:
  79. f.write('from __future__ import annotations\n')
  80. f.write('\n')
  81. f.write('from typing import TYPE_CHECKING, List, Optional, overload\n')
  82. f.write('\n')
  83. f.write('if TYPE_CHECKING:\n')
  84. f.write(' from .element import Element\n')
  85. for property in sorted(properties, key=lambda p: p.title):
  86. if not property.members:
  87. continue
  88. f.write(f' from .tailwind_types.{property.snake_title} import {property.pascal_title}\n')
  89. f.write('\n')
  90. f.write('\n')
  91. f.write('class PseudoElement:\n')
  92. f.write('\n')
  93. f.write(' def __init__(self) -> None:\n')
  94. f.write(' self._classes: List[str] = []\n')
  95. f.write('\n')
  96. f.write(' def classes(self, add: str) -> None:\n')
  97. f.write(' self._classes.append(add)\n')
  98. f.write('\n')
  99. f.write('\n')
  100. f.write('class Tailwind:\n')
  101. f.write('\n')
  102. f.write(" def __init__(self, _element: Optional['Element'] = None) -> None:\n")
  103. f.write(' self.element = _element or PseudoElement()\n')
  104. f.write('\n')
  105. f.write(' @overload\n')
  106. f.write(' def __call__(self, Tailwind) -> Tailwind:\n')
  107. f.write(' ...\n')
  108. f.write('\n')
  109. f.write(' @overload\n')
  110. f.write(' def __call__(self, *classes: str) -> Tailwind:\n')
  111. f.write(' ...\n')
  112. f.write('\n')
  113. f.write(' def __call__(self, *args) -> Tailwind:\n')
  114. f.write(' if isinstance(args[0], Tailwind):\n')
  115. f.write(' args[0].apply(self.element)\n')
  116. f.write(' else:\n')
  117. f.write(" self.element.classes(' '.join(args))\n")
  118. f.write(' return self\n')
  119. f.write('\n')
  120. f.write(" def apply(self, element: 'Element') -> None:\n")
  121. f.write(' element._classes.extend(self.element._classes)\n')
  122. f.write(' element.update()\n')
  123. for property in properties:
  124. f.write('\n')
  125. if property.members:
  126. f.write(f" def {property.snake_title}(self, value: {property.pascal_title}) -> 'Tailwind':\n")
  127. f.write(f' """{property.description}"""\n')
  128. f.write(f" self.element.classes('{property.common_prefix}' + value)\n")
  129. f.write(f' return self\n')
  130. else:
  131. f.write(f" def {property.snake_title}(self) -> 'Tailwind':\n")
  132. f.write(f' """{property.description}"""\n')
  133. f.write(f" self.element.classes('{property.common_prefix}')\n")
  134. f.write(f' return self\n')