fetch_tailwind.py 6.6 KB

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