fetch_tailwind.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  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. from secure import SecurePath
  9. @dataclass
  10. class Property:
  11. title: str
  12. description: str
  13. members: List[str]
  14. short_members: List[str] = field(init=False)
  15. common_prefix: str = field(init=False)
  16. def __post_init__(self) -> None:
  17. words = [s.split('-') for s in self.members]
  18. prefix = words[0]
  19. for w in words:
  20. i = 0
  21. while i < len(prefix) and i < len(w) and prefix[i] == w[i]:
  22. i += 1
  23. prefix = prefix[:i]
  24. if not prefix:
  25. break
  26. self.short_members = ['-'.join(word[len(prefix):]) for word in words]
  27. self.common_prefix = '-'.join(prefix) + '-' if prefix else ''
  28. if len(self.short_members) == 1:
  29. if self.title == 'Container':
  30. self.members.clear()
  31. self.short_members.clear()
  32. self.common_prefix = 'container'
  33. elif self.title in {'List Style Image', 'Content', 'Appearance'}:
  34. self.short_members = ['none']
  35. self.common_prefix = self.members[0].removesuffix('-none')
  36. else:
  37. raise ValueError(f'Unknown single-value property "{self.title}"')
  38. @property
  39. def pascal_title(self) -> str:
  40. return ''.join(word.capitalize() for word in re.sub(r'[-/ &]', ' ', self.title).split())
  41. @property
  42. def snake_title(self) -> str:
  43. return '_'.join(word.lower() for word in re.sub(r'[-/ &]', ' ', self.title).split())
  44. properties: List[Property] = []
  45. def get_soup(url: str) -> BeautifulSoup:
  46. path = Path('/tmp') / url.split('/')[-1]
  47. if path.exists():
  48. html = path.read_text()
  49. else:
  50. req = requests.get(url)
  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. for property in properties:
  71. if not property.members:
  72. continue
  73. with SecurePath(open(Path(__file__).parent / 'nicegui' / 'tailwind_types' / f'{property.snake_title}.py', 'w')) as f:
  74. f.write('from typing_extensions import Literal\n')
  75. f.write('\n')
  76. f.write(f'{property.pascal_title} = Literal[\n')
  77. for short_member in property.short_members:
  78. f.write(f" '{short_member}',\n")
  79. f.write(']\n')
  80. with open(Path(__file__).parent / 'nicegui' / 'tailwind.py', 'w') as f:
  81. f.write('from __future__ import annotations\n')
  82. f.write('\n')
  83. f.write('from typing import TYPE_CHECKING, List, Optional, overload\n')
  84. f.write('\n')
  85. f.write('if TYPE_CHECKING:\n')
  86. f.write(' from .element import Element\n')
  87. for property in sorted(properties, key=lambda p: p.title):
  88. if not property.members:
  89. continue
  90. f.write(f' from .tailwind_types.{property.snake_title} import {property.pascal_title}\n')
  91. f.write('\n')
  92. f.write('\n')
  93. f.write('class PseudoElement:\n')
  94. f.write('\n')
  95. f.write(' def __init__(self) -> None:\n')
  96. f.write(' self._classes: List[str] = []\n')
  97. f.write('\n')
  98. f.write(' def classes(self, add: str) -> None:\n')
  99. f.write(' self._classes.append(add)\n')
  100. f.write('\n')
  101. f.write('\n')
  102. f.write('class Tailwind:\n')
  103. f.write('\n')
  104. f.write(" def __init__(self, _element: Optional['Element'] = None) -> None:\n")
  105. f.write(' self.element = _element or PseudoElement()\n')
  106. f.write('\n')
  107. f.write(' @overload\n')
  108. f.write(' def __call__(self, Tailwind) -> Tailwind:\n')
  109. f.write(' ...\n')
  110. f.write('\n')
  111. f.write(' @overload\n')
  112. f.write(' def __call__(self, *classes: str) -> Tailwind:\n')
  113. f.write(' ...\n')
  114. f.write('\n')
  115. f.write(' def __call__(self, *args) -> Tailwind:\n')
  116. f.write(' if isinstance(args[0], Tailwind):\n')
  117. f.write(' args[0].apply(self.element)\n')
  118. f.write(' else:\n')
  119. f.write(" self.element.classes(' '.join(args))\n")
  120. f.write(' return self\n')
  121. f.write('\n')
  122. f.write(" def apply(self, element: 'Element') -> None:\n")
  123. f.write(' element._classes.extend(self.element._classes)\n')
  124. f.write(' element.update()\n')
  125. for property in properties:
  126. f.write('\n')
  127. if property.members:
  128. f.write(f" def {property.snake_title}(self, value: {property.pascal_title}) -> 'Tailwind':\n")
  129. f.write(f' """{property.description}"""\n')
  130. f.write(f" self.element.classes('{property.common_prefix}' + value)\n")
  131. f.write(f' return self\n')
  132. else:
  133. f.write(f" def {property.snake_title}(self) -> 'Tailwind':\n")
  134. f.write(f' """{property.description}"""\n')
  135. f.write(f" self.element.classes('{property.common_prefix}')\n")
  136. f.write(f' return self\n')