#!/usr/bin/env python3 # -*- coding: utf-8 -*- # См заметку https://eax.me/2025/2025-11-14-static-blog.html import os import re import glob import argparse def parse_arguments(): """Парсинг аргументов командной строки""" parser = argparse.ArgumentParser( description="Применить типографические улучшения к постам блога" ) group = parser.add_mutually_exclusive_group(required=True) group.add_argument( "--all", action="store_true", help="Обработать все посты во всех каталогах с годами" ) group.add_argument( "--file", metavar="FILEPATH", help="Обработать только указанный файл" ) return parser.parse_args() def find_year_directories(): """Найти все каталоги с годами (например, 2024, 2025, но не _2025)""" year_dirs = [] for item in os.listdir("."): if os.path.isdir(item) and re.match(r"^\d{4}$", item): year_dirs.append(item) return sorted(year_dirs) def prettify_units(content): """Заменить пробелы перед единицами измерения на неразрывные пробелы""" # Список единиц измерения (русские и английские) units = [ # Электрические единицы "В", "V", "А", "A", "Вт", "W", "Ом", "Ohm", "мВ", "кВ", "мА", "кА", "мВт", "кВт", "МВт", "кОм", "МОм", "мОм", "мкОм", "мкВ", "мкА", "мкВт", "мФ", "мкФ", "нФ", "пФ", "мГн", "мкГн", "нГн", "mV", "kV", "MV", "mA", "kA", "mW", "kW", "MW", "kOhm", "MOhm", "Vpp", "Vp", "Vrms", # Частотные единицы "Гц", "Hz", "кГц", "МГц", "ГГц", "kHz", "MHz", "GHz", "THz", # Единицы времени "с", "мс", "мкс", "нс", "пс", "s", "ms", "us", "ns", "ps", "сек", "мин", "ч", "sec", "min", "h", # Единицы длины и расстояния "м", "см", "мм", "км", "мкм", "нм", "mm", "cm", "m", "km", "um", "nm", # Единицы массы "г", "кг", "мг", "g", "kg", "mg", # Единицы температуры "K", # Единицы информации "КБ", "Кб", "МБ", "Мб", "ГБ", "Гб", "ТБ", "Тб", "KB", "MB", "GB", "TB", "байт", "бит", # Единицы скорости "м/с", "км/ч", "m/s", "km/h", "mph", # Доли "ppm", "dB", "dBm", "dBi", "дБ", "дБВт", ] for unit in units: # Ищем число (целое или с десятичной точкой/запятой) + один пробел + единица pattern = rf"(\d+(?:[.,]\d+)?) ({re.escape(unit)})\b" replacement = rf"\1 \2" content = re.sub(pattern, replacement, content) return content def prettify_numerals(content): """Заменить числительные с дефисом на числительные в неразрывных пробелах""" # Паттерн для поиска числительных с дефисом, исключая уже обернутые в nowrap # Ищем: опционально + цифры + дефис + русские буквы pattern = r'()?(\d+)-([а-яё]{1,2})' def replace_func(match): span_start = match.group(1) # или None number = match.group(2) suffix = match.group(3) # Если уже есть открывающий span тег, не изменяем if span_start: return match.group(0) # возвращаем оригинал без изменений # Иначе оборачиваем в span return f'{number}-{suffix}' content = re.sub(pattern, replace_func, content, flags=re.IGNORECASE) return content def prettify_ndash_spaces(content): """Заменить пробел перед – на неразрывный пробел""" # Заменяем обычный пробел перед – на   content = content.replace(' –', ' –') return content def save_alt_attributes(content): """Сохранить содержимое alt атрибутов и заменить их на метки""" saved_alts = {} counter = 1 def replace_alt(match): nonlocal counter alt_content = match.group(1) placeholder = f"__SAVED_ALT_TEXT_{counter}__" saved_alts[placeholder] = alt_content counter += 1 return f'alt="{placeholder}"' # Ищем все alt="..." атрибуты pattern = r'alt="([^"]*)"' processed_content = re.sub(pattern, replace_alt, content) return processed_content, saved_alts def restore_alt_attributes(content, saved_alts): """Восстановить сохраненное содержимое alt атрибутов""" for placeholder, original_content in saved_alts.items(): content = content.replace(f'"{placeholder}"', f'"{original_content}"') return content def save_code_content(content, filename): """Сохранить содержимое code тегов и заменить их на метки""" saved_codes = {} counter = 1 warning_shown = False # Флаг для отслеживания предупреждений в текущем файле def replace_code(match): nonlocal counter, warning_shown code_content = match.group(1) # Проверяем длину строк в блоке кода (только если предупреждение еще не выводилось) if not warning_shown: lines = code_content.split('\n') for line_num, line in enumerate(lines, 1): # Декодируем только основные HTML-сущности и убираем символы перевода строки decoded_line = line.replace('<', '<').replace('>', '>').replace('"', '"').replace('&', '&').rstrip('\r\n') if len(decoded_line) > 80: print(f"Предупреждение: в файле {filename} в блоке кода строка длиннее 80 символов (hint: используйте ⏎)") print("```") print(decoded_line) print("```") warning_shown = True # Устанавливаем флаг что предупреждение уже выведено break # Выводим предупреждение только один раз на блок # Автоматически объединяем строки с символом ⏎ где это возможно i = 0 while i < len(lines) - 1: current_line = lines[i].replace('<', '<').replace('>', '>').replace('"', '"').replace('&', '&').rstrip('\r\n') next_line = lines[i + 1].replace('<', '<').replace('>', '>').replace('"', '"').replace('&', '&').rstrip('\r\n') # Проверяем, заканчивается ли текущая строка на ⏎ if current_line.endswith('⏎'): # Вычисляем длину строки без ⏎ плюс длина следующей строки current_without_symbol = current_line[:-1] # убираем ⏎ combined_length = len(current_without_symbol) + len(next_line) if combined_length <= 80: # Объединяем строки: убираем ⏎ из текущей строки и добавляем следующую # Нужно восстановить оригинальные HTML-сущности original_current = lines[i].rstrip('\r\n') original_next = lines[i + 1].rstrip('\r\n') # Убираем ⏎ из конца текущей строки и добавляем следующую if original_current.endswith('⏎'): combined_line = original_current[:-1] + original_next lines[i] = combined_line lines.pop(i + 1) # Удаляем следующую строку # Не увеличиваем i, проверяем ту же строку еще раз continue i += 1 # Восстанавливаем содержимое после потенциального объединения строк modified_code_content = '\n'.join(lines) placeholder = f"__SAVED_CODE_CONTENT_{counter}__" saved_codes[placeholder] = modified_code_content counter += 1 return f"{placeholder}" # Ищем все ... теги pattern = r'(.*?)' processed_content = re.sub(pattern, replace_code, content, flags=re.DOTALL) return processed_content, saved_codes def restore_code_content(content, saved_codes): """Восстановить сохраненное содержимое code тегов""" for placeholder, original_content in saved_codes.items(): content = content.replace(placeholder, original_content) return content def prettify_prepositions(content): """Заменить пробелы после предлогов на неразрывные пробелы""" # Список предлогов, союзов и местоимений short_words = [ # Предлоги и союзы "в", "и", "а", "к", "о", "с", "у", "на", "за", "из", "до", "от", "по", "со", "во", "об", "то", "но", "не", "см", # Местоимения "я", "ты", "он", "мы", "вы", "им", "их", "её", "ее", "ей", ] for word in short_words: # Используем case-insensitive regex для обработки разных регистров (только один пробел) pattern = rf"\b({word}) " def replace_func(match): matched_word = match.group(1) return f"{matched_word} " content = re.sub(pattern, replace_func, content, flags=re.IGNORECASE) # Обрабатываем единицы измерения content = prettify_units(content) # Обрабатываем числительные с дефисом content = prettify_numerals(content) # Обрабатываем пробелы перед – content = prettify_ndash_spaces(content) return content def process_post_file(filepath): """Обработать один файл поста""" filename = os.path.basename(filepath) # Убираем вывод для каждого файла - будем выводить только измененные # Читаем содержимое файла try: with open(filepath, "r", encoding="utf-8") as f: content = f.read() except IOError as e: print(f"Ошибка чтения файла {filepath}: {e}") return False # Ищем содержимое тега
article_match = re.search(r'(
)(.*?)(
)', content, re.DOTALL) if not article_match: # Если тег
не найден, пропускаем файл print(f"Ошибка: в файле {filename} не найден тег
") return False # Извлекаем части before_article = content[:article_match.start(1)] article_open = article_match.group(1) article_content = article_match.group(2) article_close = article_match.group(3) after_article = content[article_match.end(3):] # Сохраняем оригинальное содержимое статьи для сравнения original_article_content = article_content # Сохраняем alt атрибуты и code теги перед обработкой protected_content, saved_alts = save_alt_attributes(article_content) protected_content, saved_codes = save_code_content(protected_content, filename) # Применяем типографические улучшения только к содержимому статьи processed_article_content = prettify_prepositions(protected_content) # Восстанавливаем code теги и alt атрибуты processed_article_content = restore_code_content(processed_article_content, saved_codes) processed_article_content = restore_alt_attributes(processed_article_content, saved_alts) # Проверяем, были ли изменения в содержимом статьи if processed_article_content == original_article_content: return False # Статья не изменена # Восстанавливаем полный контент с обработанной статьей new_content = before_article + article_open + processed_article_content + article_close + after_article # Сохраняем результат обратно в файл try: with open(filepath, "w", encoding="utf-8") as f: f.write(new_content) print(f"Изменен файл: {filename}") return True except IOError as e: print(f"Ошибка записи файла {filepath}: {e}") return False def process_year_directory(year_dir): """Обработать все посты в каталоге года""" # Ищем все HTML файлы в каталоге, исключая index.html pattern = os.path.join(year_dir, "*.html") html_files = [f for f in glob.glob(pattern) if not f.endswith("index.html")] if not html_files: return 0, 0 # (обработано, изменено) read_count = 0 modified_count = 0 for filepath in sorted(html_files): read_count += 1 if process_post_file(filepath): modified_count += 1 return read_count, modified_count def main(): """Основная функция""" args = parse_arguments() if args.all: # Режим обработки всех файлов print("Начинаем типографические улучшения постов блога") # Находим каталоги с годами year_dirs = find_year_directories() if not year_dirs: print("Не найдено каталогов с годами") return print(f"Найдены каталоги с годами: {', '.join(year_dirs)}") total_read = 0 total_modified = 0 # Обрабатываем каждый каталог с годом for year_dir in year_dirs: read_count, modified_count = process_year_directory(year_dir) total_read += read_count total_modified += modified_count print(f"\n--- Завершено ---") print(f"Всего прочитано {total_read} файлов, изменено {total_modified} файлов.") elif args.file: # Режим обработки одного файла filepath = args.file # Проверяем, существует ли файл if not os.path.exists(filepath): print(f"Ошибка: файл {filepath} не найден") return # Проверяем, что это HTML файл if not filepath.endswith('.html'): print(f"Ошибка: файл {filepath} не является HTML файлом") return print(f"Обрабатываем файл: {filepath}") # Обрабатываем файл if process_post_file(filepath): print(f"Файл {filepath} был изменен") else: print(f"Файл {filepath} не требует изменений") if __name__ == "__main__": main()