HomeDashboard/.venv/lib/python3.12/site-packages/nicegui/vbuild.py
2026-01-03 14:54:18 +01:00

112 lines
4.3 KiB
Python

from __future__ import annotations
import re
from html.parser import HTMLParser
from pathlib import Path
VOID_TAGS = 'area base br col embed hr img input link meta param source track wbr'.split()
class VBuild:
def __init__(self, filepath: Path) -> None:
parser = VueParser(filepath)
if parser.html is None:
raise ValueError(f'{filepath} has no template')
name = filepath.stem.replace('/', '-').replace('\\', '-').replace(':', '-').replace('.', '-')
html = re.sub(r'^<([\w-]+)', rf'<\1 data-{name}', parser.html)
self.html = f'<script type="text/x-template" id="tpl-{name}">\n {html}\n</script>'
self.style = '\n'.join(add_css_prefix(style, '') for style in parser.styles) + '\n'
self.style += '\n'.join(add_css_prefix(style, f'*[data-{name}]') for style in parser.scopedStyles)
self.script = parser.script or ''
class VueParser(HTMLParser): # pylint: disable=abstract-method # pylint assumes there is an abstract ``error`` method
def __init__(self, filepath: Path):
HTMLParser.__init__(self)
self.rootTag: str | None = None
self.html: str | None = None
self.script: str | None = None
self.styles: list[str] = []
self.scopedStyles: list[str] = []
self._p1: int | None = None
self._level = 0
self._tag = ''
self.feed(filepath.read_text(encoding='utf-8').strip('\n\r\t '))
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
self._tag = tag
if tag in VOID_TAGS:
return # don't manage if it's a void element
self._level += 1
if self._level == 1 and tag == 'template':
if self._p1 is not None:
raise ValueError('File contains more than one template')
self._p1 = self._get_offset() + len(self._start_tag_text)
if self._level == 2 and self._p1: # test p1, to be sure to be in a template
if self.rootTag is not None:
raise ValueError('File has more than one top level tag')
self.rootTag = tag
def handle_endtag(self, tag: str) -> None:
if tag not in VOID_TAGS:
if tag == 'template' and self._p1: # don't watch the level (so it can accept malformed HTML)
self.html = self.rawdata[self._p1:self._get_offset()].strip()
self._level -= 1
def handle_data(self, data: str) -> None:
if self._level == 1:
if self._tag == 'script':
self.script = data
if self._tag == 'style':
if 'scoped' in self._start_tag_text.lower():
self.scopedStyles.append(data)
else:
self.styles.append(data)
def _get_offset(self) -> int:
pos = 0
for _ in range(self.lineno - 1):
pos = self.rawdata.find('\n', pos) + 1
return pos + self.offset
@property
def _start_tag_text(self) -> str:
text = self.get_starttag_text()
assert text is not None
return text
def add_css_prefix(css: str, prefix: str) -> str:
"""Add the prefix (CSS selector) to all rules in ``css``."""
media_queries: list[tuple[str, str]] = []
while '@media' in css:
p1 = css.find('@media', 0)
p2 = css.find('{', p1) + 1
level = 1
while level > 0:
level += 1 if css[p2] == '{' else -1 if css[p2] == '}' else 0
p2 += 1
block = css[p1:p2]
media_def = block[:block.find('{')].strip()
media_css = block[block.find('{') + 1:block.rfind('}')].strip()
css = css.replace(block, '')
media_queries.append((media_def, add_css_prefix(media_css, prefix)))
lines: list[str] = []
css = re.sub(re.compile(r'/\*.*?\*/', re.DOTALL), '', css)
css = re.sub(re.compile(r'[ \t\n]+', re.DOTALL), ' ', css)
for rule in re.findall(r'[^}]+{[^}]+}', css):
selectors, declarations = rule.split('{', 1)
if prefix:
line = [(prefix + ' ' + i.replace(':scope', '').strip()).strip() for i in selectors.split(',')]
else:
line = [i.strip() for i in selectors.split(',')]
lines.append(', '.join(line) + ' {' + declarations.strip())
lines.extend(f'{d} {{ {c} }}' for d, c in media_queries)
return '\n'.join(lines).strip()