Текстовый файл из учебного комплекта, не свидетельство установленного runtime. Вернуться к учебнику · Исходный файл
1 """Строит локальный индекс: главы и текстовые файлы только из allowlist комплекта. 2 3 Индекс встраивается в HTML. При чтении не нужны сервер, сеть, исходные checkout 4 или чтение соседних пользовательских файлов. Секреты в комплект не добавляются. 5 """ 6 from html.parser import HTMLParser 7 import json 8 from pathlib import PurePosixPath 9 import re 10 11 12 class ChapterText(HTMLParser): 13 """Собирает видимый текст, сохраняя ссылки на ближайший заголовок главы.""" 14 15 def __init__(self, identifier): 16 super().__init__(convert_charrefs=True) 17 self.identifier = identifier 18 self.blocks = [] 19 self.current = None 20 self.heading = None 21 self.skip = 0 22 23 def handle_starttag(self, tag, attrs): 24 attrs = dict(attrs) 25 if tag in {'script', 'style', 'button'}: 26 self.skip += 1 27 if self.skip: 28 return 29 if tag in {'h1', 'h2', 'h3'} and attrs.get('id'): 30 self.current = {'href': '#' + attrs['id'], 'title': '', 'parts': []} 31 self.blocks.append(self.current) 32 self.heading = tag 33 if self.current and tag in {'p', 'div', 'li', 'tr', 'td', 'th', 'br', 'pre', 'text', 'h4', 'dt', 'dd'}: 34 self.current['parts'].append('\n') 35 36 def handle_endtag(self, tag): 37 if tag in {'script', 'style', 'button'}: 38 self.skip = max(0, self.skip - 1) 39 if self.skip: 40 return 41 if tag == self.heading: 42 self.heading = None 43 if self.current and tag in {'p', 'li', 'tr', 'td', 'th', 'text', 'h1', 'h2', 'h3', 'h4'}: 44 self.current['parts'].append('\n') 45 46 def handle_data(self, data): 47 if self.skip or self.current is None: 48 return 49 self.current['parts'].append(data) 50 if self.heading: 51 self.current['title'] += data 52 53 def entries(self, chapter_title): 54 for block in self.blocks: 55 text = re.sub(r'\s+', ' ', ''.join(block['parts'])).strip() 56 yield {'kind': 'chapter', 'title': block['title'].strip(), 57 'chapter': chapter_title, 'path': 'index.html', 58 'href': block['href'], 'text': text} 59 60 61 def text_files(files): 62 """Выбирает весь UTF-8-текст, кроме дубликатов HTML и служебных seed-копий. 63 64 PNG/WAV/MP3 и другие бинарные данные не являются словарём. Подписи встроенных 65 схем уже входят в главы. Manifest создаётся после индекса и проверяет его. 66 """ 67 for name, data in sorted(files.items()): 68 if name.startswith('tooling/inputs/') or name in { 69 '.voice-textbook-bundle', 'BUNDLE-MANIFEST.json', 'tooling/chapters.html', 70 'AUDIO-README.md', 'index.html', 'audio-index.html', 71 } or PurePosixPath(name).suffix == '.html': 72 continue 73 try: 74 text = data.decode('utf-8') 75 except UnicodeDecodeError: 76 continue 77 if '\0' in text or not text.strip(): 78 continue 79 yield name, text 80 81 82 def build_index(body, files): 83 # Главы индексируются по подразделам; файл кода - целиком, чтобы поиск мог 84 # сопоставить имя функции, параметры и комментарии из разных строк файла. 85 entries = [] 86 chapter_count = 0 87 for match in re.finditer(r'<section id="([^"]+)" class="chapter"[^>]*>(.*?)</section>', body, re.S): 88 parser = ChapterText(match[1]) 89 parser.feed(match[2]) 90 chapter_count += 1 91 chapter_title = parser.blocks[0]['title'].strip() if parser.blocks else match[1] 92 entries.extend(parser.entries(chapter_title)) 93 file_count = 0 94 for name, text in text_files(files): 95 file_count += 1 96 entries.append({'kind': 'file', 'title': PurePosixPath(name).name, 97 'path': name, 'href': name + '.html', 'text': text}) 98 return {'format': 1, 'chapters': chapter_count, 'files': file_count, 'entries': entries} 99 100 101 def inline_json(value): 102 # Даже пример с закрывающим тегом script остаётся данными, не HTML-кодом. 103 return json.dumps(value, ensure_ascii=False, separators=(',', ':')).replace( 104 '&', '\\u0026').replace('<', '\\u003c').replace('>', '\\u003e').replace( 105 '\u2028', '\\u2028').replace('\u2029', '\\u2029')