/
kuzant24
/
libre-macros
Обзор
Документация
Войти
/
kuzant24
/
libre-macros
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
normilize_function
macro-lib/pythonpath/libre_macros_param_codec.py
2 028 строк
73 KB
direct-dev.ru
to_work
28 июл 2026, 05:31
28 июл 2026, 05:31
b797194
Код
Авторство
О чём код?
# -*- coding: utf-8 -*- """ Единый кодек параметров постобработки / финала — только JSON. param_decode(fn_key, raw_text) -> list[dict] param_encode(fn_key, blocks) -> str """ from __future__ import print_function, unicode_literals MACRO_VERSION = "3.10.295" import json import re try: unicode except NameError: unicode = str PARAM_CODEC_VERSION = 1 DS_LIST = "," _FN_ALIASES = { "раскрасить": "раскрасить_блоки", "условное_форматирование": "подсветка_по_порогу", "подкрасить_пороги": "подсветка_по_порогу", "пропуск_пустых_строк": "удаление_строк", "пропуск_строк_источника": "пропуск_строк_источника", "удалить_листы": "удаление_листов", "удалить_служебные_листы": "удаление_листов", "нормализация_текста": "текстовые_операции", "нормализовать_текст": "текстовые_операции", "normalize_text": "текстовые_операции", } _SHEET_BLOCK_FORM_FNS = frozenset( { "градиент", "подсветка_по_порогу", "формат_даты", "формат_деньги", "удалить_столбцы", "сетка", "высота_строки", "отступ", "шрифт", "ширина_столбцов", "перенос", "перенос_и_авто_высота", "заголовок_плюс_высота", "зебра_диапазон", "конкатенация_столбцов", "применить_формулу", "удаление_строк", "группировка_по_столбцу", "стиль_печати", "заполнение_вниз", "копировать_значения", "замена_значений", "переименовать_лист", "переименовать_столбцы", "переставить_столбцы", "количество_значений", "копировать_лист", "преобразовать_числа", "цвет_текста_по_значению", "удаление_верхних_строк", "ячейка_в_столбец", "пропуск_строк_источника", } ) _SHEET_BLOCK_RULES_FNS = frozenset({"формат_столбцы", "заполнение_вниз_вычислить"}) _SHEET_LIST_FNS = frozenset( { "тонкая_сетка", "толстая_сетка", "авто_высота", "авто_ширина", "левое_выравнивание", "сброс_стрипов", "закрепить_заголовок", "автофильтр", "вертикаль_центр", "только_значения", "удаление_листов", "скрытие_листов", "серые_служебные", "полосы_по_пути", "чередующиеся_границы", "копировать_формат_заголовка", "подсветка_по_заголовку", "жирный_по_пути", } ) _PIVOT_FIELDS = ( "source_sheet", "header_row", "data_start", "data_end", "sheet_name", "as_values", "row_fields", "column_fields", "filter_fields", "data_fields", ) def normalize_fn_key(fn_key): key = unicode(fn_key or "").strip() if key == "": return key return _FN_ALIASES.get(key.casefold(), key) def is_sheet_list_fn(fn_key): return normalize_fn_key(fn_key) in _SHEET_LIST_FNS def sheet_list_allows_convert(fn_key): return False def is_sheet_block_form_fn(fn_key): return normalize_fn_key(fn_key) in _SHEET_BLOCK_FORM_FNS def is_sheet_block_rules_fn(fn_key): return normalize_fn_key(fn_key) in _SHEET_BLOCK_RULES_FNS def sheet_block_form_allows_convert(fn_key): return normalize_fn_key(fn_key) in ( "формат_даты", "формат_деньги", "формат_столбцы", ) def is_codec_fn(fn_key): fn_key = normalize_fn_key(fn_key) if fn_key in ( "сортировка", "раскрасить_блоки", "объединить_листы_в_один", "разделить_листы", ): return True if is_sheet_list_fn(fn_key) or is_sheet_block_form_fn(fn_key): return True if is_sheet_block_rules_fn(fn_key): return True return False def _normalize_block_for_fn(fn_key, block): fn_key = normalize_fn_key(fn_key) if fn_key == "сортировка": return _normalize_sort_block(block) if fn_key == "раскрасить_блоки": return _normalize_colorize_block(block) if fn_key == "сводная_таблица": return _normalize_pivot_block(block) if is_sheet_list_fn(fn_key): return _normalize_sheet_list_block(fn_key, block) if fn_key == "градиент": return _normalize_gradient_block(block) if fn_key == "подсветка_по_порогу": return _normalize_threshold_block(block) if fn_key == "формат_деньги": return _normalize_format_money_block(block) if fn_key == "формат_даты": return _normalize_format_date_block(block) if fn_key == "удалить_столбцы": return _normalize_delete_columns_block(block) if fn_key == "сетка": return _normalize_grid_block(block) if fn_key == "формат_столбцы": return _normalize_format_columns_block(block) if fn_key == "высота_строки": return _normalize_row_height_block(block) if fn_key == "отступ": return _normalize_indent_block(block) if fn_key == "шрифт": return _normalize_font_block(block) if fn_key == "ширина_столбцов": return _normalize_column_width_block(block) if fn_key in ("перенос", "перенос_и_авто_высота"): return _normalize_wrap_block(fn_key, block) if fn_key == "заголовок_плюс_высота": return _normalize_header_plus_height_block(block) if fn_key == "зебра_диапазон": return _normalize_zebra_block(block) if fn_key == "конкатенация_столбцов": return _normalize_concat_columns_block(block) if fn_key == "применить_формулу": return _normalize_apply_formula_block(block) if fn_key == "удаление_строк": return _normalize_delete_rows_block(block) if fn_key == "пропуск_строк_источника": return _normalize_skip_source_rows_block(block) if fn_key == "группировка_по_столбцу": return _normalize_group_by_column_block(block) if fn_key == "стиль_печати": return _normalize_print_style_block(block) if fn_key == "заполнение_вниз": return _normalize_fill_down_block(block) if fn_key == "заполнение_вниз_вычислить": return _normalize_fill_down_calculate_block(block) if fn_key == "копировать_значения": return _normalize_copy_values_block(block) if fn_key == "замена_значений": return _normalize_replace_values_block(block) if fn_key == "текстовые_операции": return _normalize_normalize_text_block(block) if fn_key == "переименовать_лист": return _normalize_rename_sheet_block(block) if fn_key == "переименовать_столбцы": return _normalize_rename_columns_block(block) if fn_key == "переставить_столбцы": return _normalize_reorder_columns_block(block) if fn_key == "количество_значений": return _normalize_value_count_block(block) if fn_key == "копировать_лист": return _normalize_copy_sheet_block(block) if fn_key == "объединить_листы_в_один": return _normalize_merge_sheets_into_one_block(block) if fn_key == "разделить_листы": return _normalize_split_sheets_block(block) if fn_key == "преобразовать_числа": return _normalize_convert_numeric_block(block) if fn_key == "цвет_текста_по_значению": return _normalize_text_color_by_value_block(block) if fn_key == "удаление_верхних_строк": return _normalize_delete_top_rows_block(block) if fn_key == "ячейка_в_столбец": return _normalize_cell_to_column_block(block) return _normalize_block(fn_key, block) def _looks_like_json_payload(raw): s = unicode(raw or "").strip() return bool(s) and s[0] in "[{" def _parse_json_relaxed(text): """ Разбор JSON без libre_macros_pivot_lib (избегаем циклического импорта pivot_lib ↔ libre_macros_lib при первом открытии диалога в LO). """ s = unicode(text or "").strip() if s == "": return None # Частый кейс из Calc: строка выглядит как JSON, но с удвоенными кавычками, # например: "[{""v"":1,""colors"":[""a"",""b""]}]". # Это невалидный JSON, но легко чинится заменой "" → " (после снятия внешних "). try: if len(s) >= 2 and s[0] == u"\"" and s[-1] == u"\"" and u"\"\"" in s: unq = unicode(s[1:-1]) fixed_quotes = unq.replace(u"\"\"", u"\"").strip() if fixed_quotes and fixed_quotes[0] in u"[{": try: return json.loads(fixed_quotes) except (ValueError, TypeError): pass except Exception: pass try: return json.loads(s) except (ValueError, TypeError): pass # Если строка не обёрнута во внешние кавычки, но содержит "" внутри — тоже пробуем починить. if u"\"\"" in s and (u"{\"\"" in s or u"[{\"\"" in s): try: fixed_quotes = s.replace(u"\"\"", u"\"") return json.loads(fixed_quotes) except (ValueError, TypeError): pass try: import ast val = ast.literal_eval(s) if isinstance(val, (dict, list)): return val except Exception: pass fixed = s fixed = re.sub(r"\bTrue\b", "true", fixed) fixed = re.sub(r"\bFalse\b", "false", fixed) fixed = re.sub(r"\bNone\b", "null", fixed) fixed = re.sub(r",\s*}", "}", fixed) fixed = re.sub(r",\s*]", "]", fixed) try: return json.loads(fixed) except (ValueError, TypeError): return None def _parse_json_root(raw_text): """Корень JSON: массив блоков; объект — один блок; «голый» объект сводной.""" s = unicode(raw_text or "").strip() if s == "": return [] if not _looks_like_json_payload(s): raise ValueError("not json") data = _parse_json_relaxed(s) if data is None: raise ValueError("json parse failed") if isinstance(data, list): return data if isinstance(data, dict): return [data] raise ValueError("JSON root must be array or object") def _normalize_block(fn_key, block): if not isinstance(block, dict): raise ValueError("block must be object") # В LibreOffice (особенно со старыми Python/UNO) ключи dict иногда приходят как bytes/str # (python-literal через ast.literal_eval), а в коде здесь почти везде используются # unicode-ключи из-за unicode_literals. Приводим ключи к тексту, чтобы b.get("colors") # работал независимо от исходного типа ключа. out = {} for k, v in block.items(): try: if isinstance(k, bytes): kk = k.decode("utf-8", "ignore") else: kk = unicode(k) except Exception: try: kk = str(k) except Exception: kk = u"" out[kk] = v out["v"] = int(out.get("v") or PARAM_CODEC_VERSION) out["fn"] = normalize_fn_key(out.get("fn") or fn_key) return out def _decode_json_blocks(fn_key, items): fn_key = normalize_fn_key(fn_key) if fn_key == "сводная_таблица": return _decode_pivot_json(items) if fn_key == "сортировка": return [_normalize_sort_block(b) for b in items] if fn_key == "раскрасить_блоки": return [_normalize_colorize_block(b) for b in items] if is_sheet_list_fn(fn_key): return [_normalize_sheet_list_block(fn_key, b) for b in items] if fn_key == "градиент": return [_normalize_gradient_block(b) for b in items] if fn_key == "подсветка_по_порогу": return [_normalize_threshold_block(b) for b in items] if fn_key == "формат_деньги": return [_normalize_format_money_block(b) for b in items] if fn_key == "формат_даты": return [_normalize_format_date_block(b) for b in items] if fn_key == "удалить_столбцы": return [_normalize_delete_columns_block(b) for b in items] if fn_key == "сетка": return [_normalize_grid_block(b) for b in items] if fn_key == "формат_столбцы": return [_normalize_format_columns_block(b) for b in items] if fn_key == "высота_строки": return [_normalize_row_height_block(b) for b in items] if fn_key == "отступ": return [_normalize_indent_block(b) for b in items] if fn_key == "шрифт": return [_normalize_font_block(b) for b in items] if fn_key == "ширина_столбцов": return [_normalize_column_width_block(b) for b in items] if fn_key in ("перенос", "перенос_и_авто_высота"): return [_normalize_wrap_block(fn_key, b) for b in items] if fn_key == "заголовок_плюс_высота": return [_normalize_header_plus_height_block(b) for b in items] if fn_key == "зебра_диапазон": return [_normalize_zebra_block(b) for b in items] if fn_key == "конкатенация_столбцов": return [_normalize_concat_columns_block(b) for b in items] if fn_key == "применить_формулу": return [_normalize_apply_formula_block(b) for b in items] if fn_key == "удаление_строк": return [_normalize_delete_rows_block(b) for b in items] if fn_key == "пропуск_строк_источника": return [_normalize_skip_source_rows_block(b) for b in items] if fn_key == "группировка_по_столбцу": return [_normalize_group_by_column_block(b) for b in items] if fn_key == "стиль_печати": return [_normalize_print_style_block(b) for b in items] if fn_key == "заполнение_вниз": return [_normalize_fill_down_block(b) for b in items] if fn_key == "заполнение_вниз_вычислить": return [_normalize_fill_down_calculate_block(b) for b in items] if fn_key == "копировать_значения": return [_normalize_copy_values_block(b) for b in items] if fn_key == "переименовать_лист": return [_normalize_rename_sheet_block(b) for b in items] if fn_key == "переименовать_столбцы": return [_normalize_rename_columns_block(b) for b in items] if fn_key == "переставить_столбцы": return [_normalize_reorder_columns_block(b) for b in items] if fn_key == "количество_значений": return [_normalize_value_count_block(b) for b in items] if fn_key == "копировать_лист": return [_normalize_copy_sheet_block(b) for b in items] if fn_key == "объединить_листы_в_один": return [_normalize_merge_sheets_into_one_block(b) for b in items] if fn_key == "разделить_листы": return [_normalize_split_sheets_block(b) for b in items] if fn_key == "преобразовать_числа": return [_normalize_convert_numeric_block(b) for b in items] if fn_key == "цвет_текста_по_значению": return [_normalize_text_color_by_value_block(b) for b in items] if fn_key == "удаление_верхних_строк": return [_normalize_delete_top_rows_block(b) for b in items] if fn_key == "ячейка_в_столбец": return [_normalize_cell_to_column_block(b) for b in items] return [_normalize_block(fn_key, b) for b in items] def _decode_pivot_json(items): if len(items) == 0: return [_pivot_empty_block()] if len(items) != 1: raise ValueError("сводная_таблица: ожидается один блок") return [_normalize_pivot_block(items[0])] def _pivot_empty_block(): return {"v": PARAM_CODEC_VERSION, "fn": "сводная_таблица"} def _normalize_pivot_block(block): out = _pivot_empty_block() if not isinstance(block, dict): return out for key in _PIVOT_FIELDS: if key in block: out[key] = block[key] if block.get("as_values"): out["as_values"] = True return out def _normalize_sort_block(block): out = _normalize_block("сортировка", block) keys = out.get("keys") if keys is None: out["keys"] = [] elif not isinstance(keys, list): out["keys"] = [] norm_keys = [] for item in out["keys"]: if not isinstance(item, dict): continue col = unicode(item.get("column") or "").strip() if col == "": continue norm_keys.append({"column": col, "desc": bool(item.get("desc"))}) out["keys"] = norm_keys sheet = unicode(out.get("sheet") or "").strip() if sheet != "": out["sheet"] = sheet elif "sheet" in out: del out["sheet"] return out def _normalize_colorize_block(block): out = _normalize_block("раскрасить_блоки", block) cols = out.get("key_columns") if cols is None: out["key_columns"] = [] elif isinstance(cols, (list, tuple)): out["key_columns"] = [unicode(c).strip() for c in cols if unicode(c).strip()] else: out["key_columns"] = [unicode(cols).strip()] if unicode(cols).strip() else [] colors = out.get("colors") if colors is None: out["colors"] = [] elif isinstance(colors, (list, tuple)): out["colors"] = [unicode(c).strip() for c in colors if unicode(c).strip()] else: out["colors"] = [unicode(colors).strip()] if unicode(colors).strip() else [] sort_val = out.get("sort") if sort_val in (None, "", "none"): out["sort"] = None elif unicode(sort_val).casefold() in ("asc", "+", "возр"): out["sort"] = "asc" elif unicode(sort_val).casefold() in ("desc", "-", "убыв"): out["sort"] = "desc" else: out["sort"] = None out["outline_blocks"] = bool(out.get("outline_blocks")) oc = out.get("outline_color") if oc in (None, "", "null"): out["outline_color"] = None else: out["outline_color"] = unicode(oc).strip() sheet = unicode(out.get("sheet") or "").strip() if sheet != "": out["sheet"] = sheet elif "sheet" in out: del out["sheet"] return out def _normalize_sheet_list_block(fn_key, block): fn_key = normalize_fn_key(fn_key) out = _normalize_block(fn_key, block) sheets = out.get("sheets") if sheets is not None: if isinstance(sheets, (list, tuple)): norm = [unicode(s).strip() for s in sheets if unicode(s).strip()] else: single = unicode(sheets).strip() norm = [single] if single else [] if norm: out["sheets"] = norm else: out.pop("sheets", None) else: out.pop("sheets", None) out.pop("sheet", None) filt = out.get("filter") if filt is None: filt = out.get("lambda") or out.get("code") or out.get("predicate") filt = unicode(filt or "").strip() out.pop("lambda", None) out.pop("code", None) out.pop("predicate", None) if filt != "": out["filter"] = filt else: out.pop("filter", None) if sheet_list_allows_convert(fn_key): if out.get("convert_existing"): out["convert_existing"] = True else: out.pop("convert_existing", None) else: out.pop("convert_existing", None) return out def _normalize_delete_top_rows_block(block): """ Блок для параметра «удаление_верхних_строк». Поля: - sheet: имя листа результата - n: число строк для удаления (int > 0) """ out = _normalize_block("удаление_верхних_строк", block) # sheet sheet = unicode(out.get("sheet") or "").strip() if sheet != "": out["sheet"] = sheet else: out.pop("sheet", None) # n n_raw = out.get("n") if n_raw in (None, ""): n_raw = out.get("rows") if n_raw in (None, ""): n_raw = out.get("count") if n_raw in (None, ""): n_raw = out.get("value") try: n = int(n_raw) except Exception: n = 0 if n > 0: out["n"] = n else: out.pop("n", None) # cleanup legacy out.pop("rows", None) out.pop("count", None) out.pop("value", None) return out def _normalize_cell_to_column_block(block): """Блок «Ячейка_В_Столбец»: col, row (1-based), column, sheet, source_sheet.""" out = _normalize_block("ячейка_в_столбец", block) col_s = unicode(out.get("col") or out.get("src_col") or "").strip() if col_s != "": out["col"] = col_s else: out.pop("col", None) out.pop("src_col", None) try: row_n = int(out.get("row", out.get("r", 1))) except Exception: row_n = 1 if row_n > 0: out["row"] = row_n out.pop("r", None) column = unicode(out.get("column") or out.get("name") or "").strip() if column != "": out["column"] = column else: out.pop("column", None) out.pop("name", None) sheet = unicode( out.get("sheet") or out.get("sheet_name") or out.get("dest_sheet") or "" ).strip() if sheet != "": out["sheet"] = sheet else: out.pop("sheet", None) out.pop("sheet_name", None) out.pop("dest_sheet", None) source_sheet = unicode( out.get("source_sheet") or out.get("src_sheet") or out.get("sourceSheet") or "" ).strip() if source_sheet != "": out["source_sheet"] = source_sheet else: out.pop("source_sheet", None) out.pop("src_sheet", None) out.pop("sourceSheet", None) return out def _sheet_block_base(fn_key, sheet_name): block = {"v": PARAM_CODEC_VERSION, "fn": normalize_fn_key(fn_key)} sheet = unicode(sheet_name or "").strip() if sheet != "": block["sheet"] = sheet return block def _attach_sheet(block, sheet_name): sheet = unicode(sheet_name or "").strip() if sheet != "": block["sheet"] = sheet else: block.pop("sheet", None) return block def _normalize_markers_list(value): if value is None: return [] parts = [] if isinstance(value, (list, tuple)): seq = value else: text = unicode(value).strip() if text == "": return [] seq = text.split(DS_LIST) for v in seq: chunk = unicode(v).strip() if chunk == "": continue # «11,12» одним элементом списка if DS_LIST in chunk or ";" in chunk: for p in chunk.replace(";", DS_LIST).split(DS_LIST): p = p.strip() if p: parts.append(p) else: parts.append(chunk) return parts def _normalize_gradient_block(block): out = _normalize_block("градиент", block) out["marker"] = unicode(out.get("marker") or "сумм").strip() or "сумм" out["color_min"] = unicode(out.get("color_min") or "зеленый").strip() or "зеленый" out["color_max"] = unicode(out.get("color_max") or "красный").strip() or "красный" wr = block.get("whole_row") if wr is True or wr is False: if wr: out["whole_row"] = True else: try: from libre_macros_lib import lm_parse_bool_param if lm_parse_bool_param(wr, default=False): out["whole_row"] = True except Exception: if wr: out["whole_row"] = True st = block.get("sort") if st is True: out["sort"] = True elif st is not False and st is not None: try: from libre_macros_lib import lm_parse_bool_param if lm_parse_bool_param(st, default=False): out["sort"] = True except Exception: if st: out["sort"] = True if out.get("sort"): sort_dir = unicode(block.get("sort_dir") or u"по_возр").strip().lower() if sort_dir in (u"по_убыв", u"убыв", u"desc", u"-", u"убывание"): out["sort_dir"] = u"по_убыв" else: out["sort_dir"] = u"по_возр" return _attach_sheet(out, out.get("sheet")) def _normalize_threshold_block(block): out = _normalize_block("подсветка_по_порогу", block) # Столбцы: columns / markers / legacy marker columns = _normalize_column_token_list(out.get("columns")) if not columns: markers = _normalize_markers_list(out.get("markers")) if markers: columns = list(markers) marker = unicode(out.get("marker") or u"").strip() if not columns and marker: columns = [marker] if columns: out["columns"] = columns out["marker"] = unicode(columns[0]) else: out.pop("columns", None) if marker: out["marker"] = marker else: out["marker"] = u"итог" def _opt_float(key): raw = out.get(key) if raw is None or raw is False: return None s = unicode(raw).strip().replace(u",", u".") if s == u"": return None try: return float(s) except (TypeError, ValueError): return None tmin = _opt_float("threshold_min") if tmin is None: tmin = _opt_float("min") tmax = _opt_float("threshold_max") if tmax is None: tmax = _opt_float("max") legacy = out.get("threshold") legacy_f = None if legacy is not None and legacy is not False: try: legacy_f = float(unicode(legacy).strip().replace(u",", u".")) except (TypeError, ValueError): legacy_f = None if tmin is None and tmax is None and legacy_f is not None: out["threshold"] = legacy_f out.pop("threshold_min", None) out.pop("threshold_max", None) else: if tmin is not None: out["threshold_min"] = tmin else: out.pop("threshold_min", None) if tmax is not None: out["threshold_max"] = tmax else: out.pop("threshold_max", None) out.pop("threshold", None) # Алиасы min/max не храним — только threshold_min/max (или legacy threshold). out.pop("min", None) out.pop("max", None) color = unicode(out.get("color") or out.get("fill_color") or u"").strip() out["color"] = color or u"светло_красный" out.pop("fill_color", None) fc = unicode(out.get("font_color") or u"").strip() auto = out.get("font_color_auto") if fc: out["font_color"] = fc out["font_color_auto"] = False else: out.pop("font_color", None) if auto is False or auto in (0, u"0", u"false", u"нет", u"no", u"-"): out["font_color_auto"] = False else: out["font_color_auto"] = True if out.get("bold"): out["bold"] = True else: out.pop("bold", None) if out.get("italic"): out["italic"] = True else: out.pop("italic", None) if out.get("whole_row"): out["whole_row"] = True else: out.pop("whole_row", None) return _attach_sheet(out, out.get("sheet")) def _normalize_format_block_common(out, block): """Общие поля format/columns/markers/sheets для формат_деньги и формат_даты.""" sheets = out.get("sheets") if sheets is not None: if isinstance(sheets, (list, tuple)): norm = [unicode(s).strip() for s in sheets if unicode(s).strip()] else: single = unicode(sheets).strip() norm = [single] if single else [] if norm: out["sheets"] = norm else: out.pop("sheets", None) else: out.pop("sheets", None) columns = _normalize_column_token_list(out.get("columns")) if columns: out["columns"] = columns else: out.pop("columns", None) markers = _normalize_markers_list(out.get("markers")) if markers: out["markers"] = markers else: out.pop("markers", None) fmt = unicode(out.get("format") or u"").strip().replace(u"\xa0", u" ") if fmt: out["format"] = fmt else: out.pop("format", None) if out.get("convert_existing"): out["convert_existing"] = True else: out.pop("convert_existing", None) return out def _normalize_format_money_block(block): out = _normalize_block("формат_деньги", block) out = _normalize_format_block_common(out, block) return _attach_sheet(out, out.get("sheet")) def _normalize_format_date_block(block): out = _normalize_block("формат_даты", block) out = _normalize_format_block_common(out, block) return _attach_sheet(out, out.get("sheet")) def _normalize_delete_columns_block(block): out = _normalize_block("удалить_столбцы", block) markers = _normalize_markers_list(out.get("markers")) if markers: out["markers"] = markers else: out.pop("markers", None) return _attach_sheet(out, out.get("sheet")) def _normalize_grid_block(block): out = _normalize_block("сетка", block) width = unicode(out.get("width") or "тонкая").strip() out["width"] = width if width != "" else "тонкая" color = unicode(out.get("color") or "").strip() if color != "": out["color"] = color else: out.pop("color", None) if out.get("include_header"): out["include_header"] = True else: out.pop("include_header", None) return _attach_sheet(out, out.get("sheet")) def _normalize_format_columns_column_single(text): t = unicode(text or u"").strip() if t == u"": return None low = t.casefold() if low in (u"all", u"все", u"*"): return u"all" if t.isdigit(): return int(t) return t def _normalize_format_columns_column(col): if col is None or isinstance(col, bool): return None if isinstance(col, int): return col if isinstance(col, (list, tuple)): items = [] for item in col: n = _normalize_format_columns_column(item) if n is None: continue if n == u"all": return u"all" items.append(n) if not items: return None if len(items) == 1: return items[0] return items text = unicode(col).strip() if text == u"": return None low = text.casefold() if low in (u"all", u"все", u"*"): return u"all" if u"," in text: parts = [p.strip() for p in text.split(DS_LIST) if p.strip()] if not parts: return None items = [] for p in parts: n = _normalize_format_columns_column_single(p) if n is not None: items.append(n) if not items: return None if len(items) == 1: return items[0] return items return _normalize_format_columns_column_single(text) def _normalize_format_columns_rule(rule): if not isinstance(rule, dict): return None col_out = _normalize_format_columns_column(rule.get("column")) if col_out is None: return None fmt = unicode(rule.get("format") or "").strip() out = {"column": col_out} if fmt: out["format"] = fmt # styling fill = unicode(rule.get("fill") or rule.get("bg") or "").strip() if fill: out["fill"] = fill font = unicode(rule.get("font") or rule.get("font_color") or "").strip() if font: out["font"] = font if rule.get("font_auto") is True or rule.get("font_color_auto") is True: out["font_auto"] = True elif rule.get("font_auto") is False or rule.get("font_color_auto") is False: out["font_auto"] = False # border bw = unicode(rule.get("border_width") or rule.get("border") or "").strip() if bw: out["border_width"] = bw bc = unicode(rule.get("border_color") or "").strip() if bc: out["border_color"] = bc h_align = unicode(rule.get("h_align") or rule.get("align_h") or "").strip().lower() if h_align in ("left", "center", "right"): out["h_align"] = h_align v_align = unicode(rule.get("v_align") or rule.get("align_v") or "").strip().lower() if v_align in ("top", "center", "bottom"): out["v_align"] = v_align if rule.get("header_only") is True: out["header_only"] = True elif rule.get("include_header") is True: out["include_header"] = True return out def _normalize_format_columns_block(block): out = _normalize_block("формат_столбцы", block) rules_in = out.get("rules") norm_rules = [] if isinstance(rules_in, (list, tuple)): for item in rules_in: nr = _normalize_format_columns_rule(item) if nr is not None: norm_rules.append(nr) out["rules"] = norm_rules if out.get("convert_existing"): out["convert_existing"] = True else: out.pop("convert_existing", None) return _attach_sheet(out, out.get("sheet")) def _normalize_row_height_block(block): out = _normalize_block("высота_строки", block) rows = out.get("rows") if rows in (None, "", "all"): out["rows"] = "all" elif isinstance(rows, (list, tuple)): out["rows"] = [int(r) for r in rows if str(r).strip() != ""] else: text = unicode(rows).strip() if text == "" or text.lower() in ("all", "все"): out["rows"] = "all" else: out["rows"] = [int(p.strip()) for p in text.split(DS_LIST) if p.strip()] try: out["height_mm"] = float(out.get("height_mm")) except (TypeError, ValueError): out["height_mm"] = 5.0 return _attach_sheet(out, out.get("sheet")) def _normalize_column_width_block(block): out = _normalize_block("ширина_столбцов", block) cols = out.get("columns") if cols in (None, "", "all"): out["columns"] = "all" elif isinstance(cols, (list, tuple)): norm = [] for c in cols: if isinstance(c, int): norm.append(c) else: t = unicode(c).strip() if t.isdigit(): norm.append(int(t)) elif t: norm.append(t) out["columns"] = norm if norm else "all" else: text = unicode(cols).strip() if text == "" or text.lower() in ("all", "все"): out["columns"] = "all" else: out["columns"] = [p.strip() for p in text.split(DS_LIST) if p.strip()] try: out["width_mm"] = float(out.get("width_mm")) except (TypeError, ValueError): out["width_mm"] = 30.0 return _attach_sheet(out, out.get("sheet")) def _normalize_indent_block(block): out = _normalize_block("отступ", block) align = unicode(out.get("h_align") or "left").strip().lower() if align not in ("left", "center", "right"): align = "left" out["h_align"] = align try: out["steps"] = max(0, int(out.get("steps", 1))) except (TypeError, ValueError): out["steps"] = 1 cols = out.get("columns") if cols in (None, "", []): out.pop("columns", None) elif isinstance(cols, (list, tuple)): out["columns"] = [unicode(c).strip() for c in cols if unicode(c).strip()] else: out["columns"] = [p.strip() for p in unicode(cols).split(DS_LIST) if p.strip()] return _attach_sheet(out, out.get("sheet")) def _normalize_font_block(block): out = _normalize_block("шрифт", block) name = out.get("name") if name in (None, u"", u"-", u"—"): out.pop("name", None) else: out["name"] = unicode(name).strip() for key in ("size_data", "size_header"): val = out.get(key) if val in (None, u"", u"-", u"—"): out.pop(key, None) else: try: out[key] = float(val) except (TypeError, ValueError): out.pop(key, None) return _attach_sheet(out, out.get("sheet")) def _normalize_wrap_block(fn_key, block): out = _normalize_block(fn_key, block) if out.get("wrap") is False: out["wrap"] = False else: out["wrap"] = True if out.get("include_header"): out["include_header"] = True else: out.pop("include_header", None) return _attach_sheet(out, out.get("sheet")) def _normalize_header_plus_height_block(block): out = _normalize_block("заголовок_плюс_высота", block) try: out["height_mm"] = float(out.get("height_mm")) except (TypeError, ValueError): out["height_mm"] = 12.7 hori = unicode(out.get("h_align") or "center").strip().lower() or "center" vert = unicode(out.get("v_align") or "center").strip().lower() or "center" out["h_align"] = hori out["v_align"] = vert out["bold"] = bool(out.get("bold")) font_name = unicode(out.get("font_name") or u"").strip() out["font_name"] = font_name if font_name else u"" font_size = out.get("font_size") if font_size in (u"", u"-", u"—", None): out["font_size"] = None else: try: out["font_size"] = float(font_size) except (TypeError, ValueError): out["font_size"] = None out["fill_color"] = unicode(out.get("fill_color") or u"").strip() font_color = unicode(out.get("font_color") or u"").strip() out["font_color"] = font_color if font_color: out["font_color_auto"] = bool(out.get("font_color_auto", False)) else: out["font_color_auto"] = bool(out.get("font_color_auto", True)) return _attach_sheet(out, out.get("sheet")) def _zebra_role_labels(): return ( ("header", "header"), ("even", "even"), ("odd", "odd"), ) def _normalize_zebra_block(block): out = _normalize_block("зебра_диапазон", block) roles = out.get("roles") if isinstance(roles, dict): norm_roles = {} for role_key, label in _zebra_role_labels(): spec = roles.get(role_key) or roles.get(label) if not isinstance(spec, dict): continue fill = unicode(spec.get("fill") or "").strip() font = unicode(spec.get("font") or "").strip() if fill or font: norm_roles[role_key] = {"fill": fill, "font": font} if norm_roles: out["roles"] = norm_roles else: out.pop("roles", None) return _attach_sheet(out, out.get("sheet")) def _normalize_int_list(value): if value is None: return [] if isinstance(value, (list, tuple)): out = [] for item in value: if isinstance(item, int): out.append(item) else: s = unicode(item).strip() if s.isdigit(): out.append(int(s)) return out text = unicode(value).strip() if text == "": return [] out = [] for part in re.split(r"[,;]", text): part = part.strip() if part.isdigit(): out.append(int(part)) return out def _normalize_column_token_list(value): if value is None: return [] if isinstance(value, (list, tuple)): out = [] for item in value: if isinstance(item, int): out.append(item) else: s = unicode(item).strip() if s == "": continue if s.isdigit(): out.append(int(s)) else: out.append(s) return out text = unicode(value).strip() if text == "": return [] out = [] for part in re.split(r"[,;]", text): part = part.strip() if part == "": continue if part.isdigit(): out.append(int(part)) else: out.append(part) return out def _normalize_concat_columns_block(block): out = _normalize_block("конкатенация_столбцов", block) out["new_column"] = unicode(out.get("new_column") or "").strip() out["separator"] = unicode(out.get("separator") or "") # columns: разрешаем индексы (1-based), буквы и заголовки out["columns"] = _normalize_column_token_list(out.get("columns")) return _attach_sheet(out, out.get("sheet")) def _normalize_delete_rows_column_list(value): if value is None: return [] if isinstance(value, (list, tuple)): return [unicode(v).strip() for v in value if unicode(v).strip()] text = unicode(value).strip() if text == "": return [] return [p.strip() for p in re.split(r"[,;]", text) if p.strip()] def _strip_leading_formula_eq(text): """Убрать один ведущий '=' (и пробелы) — для IF(...)/хранения без двойного '='.""" s = unicode(text or "").strip() if s.startswith("="): s = s[1:].strip() return s def _normalize_apply_formula_block(block): out = _normalize_block("применить_формулу", block) out["column"] = unicode(out.get("column") or "").strip() out["formula"] = unicode(out.get("formula") or "").strip() fmt = unicode(out.get("format") or "").strip() if fmt == "" or fmt.casefold() == u"(глобальный)".casefold(): out.pop("format", None) else: out["format"] = fmt if "as_values" in block: try: from libre_macros_lib import lm_parse_bool_param parsed = lm_parse_bool_param(block.get("as_values"), default=None) if parsed is True: out["as_values"] = True elif parsed is False: out["as_values"] = False except Exception: if block.get("as_values"): out["as_values"] = True else: out["as_values"] = False return _attach_sheet(out, out.get("sheet")) def _normalize_delete_rows_block(block): out = _normalize_block("удаление_строк", block) mode = unicode(out.get("mode") or "columns").strip().casefold() if mode == "formula": out["mode"] = "formula" out["formula"] = _strip_leading_formula_eq(out.get("formula") or "") out.pop("columns", None) elif mode in ("none", ""): out["mode"] = "none" out.pop("columns", None) out.pop("formula", None) else: out["mode"] = "columns" out["columns"] = _normalize_delete_rows_column_list(out.get("columns")) out.pop("formula", None) return _attach_sheet(out, out.get("sheet")) def _normalize_skip_source_rows_block(block): """ Пропуск_строк_источника: sheet = имя листа *источника* (не результата). columns — номера/буквы/заголовки; без sheet — для всех листов файла. """ out = _normalize_block("пропуск_строк_источника", block) mode = unicode(out.get("mode") or "columns").strip().casefold() if mode == "formula": out["mode"] = "formula" out["formula"] = _strip_leading_formula_eq(out.get("formula") or "") out.pop("columns", None) elif mode in ("none", ""): out["mode"] = "none" out.pop("columns", None) out.pop("formula", None) else: out["mode"] = "columns" out["columns"] = _normalize_delete_rows_column_list(out.get("columns")) out.pop("formula", None) return _attach_sheet(out, out.get("sheet")) def _normalize_group_by_column_block(block): out = _normalize_block("группировка_по_столбцу", block) out["marker"] = unicode(out.get("marker") or "").strip() return _attach_sheet(out, out.get("sheet")) def _normalize_convert_numeric_block(block): out = _normalize_block("преобразовать_числа", block) cols = out.get("columns") if cols in (None, "", []): out["columns"] = [] else: out["columns"] = _normalize_column_token_list(cols) return _attach_sheet(out, out.get("sheet")) def _normalize_text_color_by_value_block(block): out = _normalize_block("цвет_текста_по_значению", block) m = unicode(out.get("marker") or u"").strip() out["marker"] = m if m != u"" else u"статус" return _attach_sheet(out, out.get("sheet")) def _normalize_print_style_block(block): out = _normalize_block("стиль_печати", block) orient = unicode(out.get("orientation") or "landscape").strip().casefold() if orient not in ("landscape", "portrait"): orient = "landscape" out["orientation"] = orient fit = out.get("fit_pages") try: out["fit_pages"] = int(fit) if fit is not None else 1 except (TypeError, ValueError): out["fit_pages"] = 1 return _attach_sheet(out, out.get("sheet")) def _normalize_fill_down_block(block): out = _normalize_block("заполнение_вниз", block) out["columns"] = _normalize_column_token_list(out.get("columns")) return _attach_sheet(out, out.get("sheet")) def _normalize_fill_down_calculate_rule(rule): if not isinstance(rule, dict): return None col = rule.get("column") if col is None: return None if isinstance(col, int): col_out = col else: col_s = unicode(col).strip() if col_s == "": return None if col_s.isdigit(): col_out = int(col_s) else: col_out = col_s formula = unicode(rule.get("formula") or "").strip() if formula == "": return None out = {"column": col_out, "formula": formula} if rule.get("expand_to_right"): out["expand_to_right"] = True return out def _normalize_fill_down_calculate_block(block): out = _normalize_block("заполнение_вниз_вычислить", block) cols = _normalize_column_token_list(out.get("columns")) if len(cols) > 0: out["columns"] = cols else: out.pop("columns", None) formula = unicode(out.get("formula") or "").strip() if formula: out["formula"] = formula else: out.pop("formula", None) rules_in = out.get("rules") norm_rules = [] if isinstance(rules_in, (list, tuple)): for item in rules_in: nr = _normalize_fill_down_calculate_rule(item) if nr is not None: norm_rules.append(nr) out["rules"] = norm_rules return _attach_sheet(out, out.get("sheet")) def _normalize_copy_values_block(block): out = _normalize_block("копировать_значения", block) if out.get("whole_sheet"): out["whole_sheet"] = True out["columns"] = [] else: out.pop("whole_sheet", None) cols = _normalize_column_token_list(out.get("columns")) out["columns"] = cols if len(cols) == 0: out.pop("columns", None) return _attach_sheet(out, out.get("sheet")) def _normalize_replace_values_block(block): out = _normalize_block("замена_значений", block) cols = _normalize_column_token_list(out.get("columns")) out["columns"] = cols def _norm_str_list(val): if val is None: return [] if isinstance(val, (list, tuple)): return [unicode(x).strip() for x in val if unicode(x).strip()] text = unicode(val).strip() if text == "": return [] return [p.strip() for p in re.split(r"[,;]", text) if p.strip()] find_list = _norm_str_list(out.get("find") or out.get("search") or out.get("patterns")) repl_raw = out.get("replace") if "replace" in out else (out.get("replacements") or out.get("to")) repl_list = _norm_str_list(repl_raw) out["find"] = find_list if repl_list: out["replace"] = repl_list elif find_list: out["replace"] = [u"_ПУСТО_"] else: out.pop("replace", None) def _norm_bool(key, *aliases): v = None if key in block: v = block.get(key) else: for a in aliases: if a in block: v = block.get(a) break if v is None: return try: from libre_macros_lib import lm_parse_bool_param parsed = lm_parse_bool_param(v, default=None) if parsed is True: out[key] = True elif parsed is False: out[key] = False except Exception: out[key] = bool(v) _norm_bool("case_insensitive", "без_учета_регистра", "ignore_case") _norm_bool("squeeze_spaces", "сжать_пробелы", "trim_spaces") return _attach_sheet(out, out.get("sheet")) def _normalize_normalize_text_block(block): out = _normalize_block("текстовые_операции", block) cols = _normalize_column_token_list(out.get("columns")) out["columns"] = cols if len(cols) == 0: out.pop("columns", None) try: from libre_macros_normalize_lib import coerce_ops_list ops = coerce_ops_list(out.get("ops") or out.get("stack") or out.get("pipeline")) except Exception: ops = out.get("ops") if isinstance(out.get("ops"), list) else [] out["ops"] = ops if len(ops) == 0: out.pop("ops", None) for key in ("non_text", "on_error", "empty_cells"): if key in out and out.get(key) is not None: raw = unicode(out.get(key)).strip() low = raw.casefold() if key == "non_text": mapping = { u"пропуск": u"skip", u"как текст": u"coerce", u"как_текст": u"coerce", u"ошибка": u"error", u"skip": u"skip", u"coerce": u"coerce", u"error": u"error", } out[key] = mapping.get(low, low) or u"skip" elif key == "on_error": mapping = { u"оставить до ошибки": u"keep", u"оставить": u"keep", u"очистить ячейку": u"empty", u"очистить": u"empty", u"остановить": u"stop", u"keep": u"keep", u"skip_cell": u"keep", u"empty": u"empty", u"stop": u"stop", } out[key] = mapping.get(low, low) or u"keep" else: out[key] = raw if "as_values" in block: try: from libre_macros_lib import lm_parse_bool_param parsed = lm_parse_bool_param(block.get("as_values"), default=True) if parsed is not None: out["as_values"] = bool(parsed) except Exception: out["as_values"] = bool(block.get("as_values")) for key in ("key", "key_file"): if key in out and out.get(key) is not None: out[key] = unicode(out.get(key)).strip() if out[key] == u"": out.pop(key, None) if "key_cell" in out and out.get("key_cell") in (u"", None): out.pop("key_cell", None) return _attach_sheet(out, out.get("sheet")) def _normalize_rename_sheet_block(block): out = _normalize_block("переименовать_лист", block) out["new_name"] = unicode(out.get("new_name") or "").strip() return _attach_sheet(out, out.get("sheet")) _REORDER_POSITION_ALIASES = { "_начало_": "start", "_конец_": "end", "_перед_": "before", "_после_": "after", "начало": "start", "конец": "end", "перед": "before", "после": "after", "start": "start", "end": "end", "before": "before", "after": "after", } def _normalize_reorder_position(raw): key = unicode(raw or "").strip().casefold() if key in _REORDER_POSITION_ALIASES: return _REORDER_POSITION_ALIASES[key] if key in ("start", "end", "before", "after"): return key return "" def _split_column_name_tokens_by_sep(text, sep): """Разбор по sep с экранированием \\, и \\;.""" s = unicode(text or "") out = [] buf = [] esc = False i = 0 while i < len(s): ch = s[i] if esc: buf.append(ch) esc = False i = i + 1 continue if ch == u"\\": esc = True i = i + 1 continue if ch == sep: t = u"".join(buf).strip() if t != "": out.append(t) buf = [] i = i + 1 continue buf.append(ch) i = i + 1 t = u"".join(buf).strip() if t != "": out.append(t) return out def detect_column_name_list_sep(text, known_names=None): """ Разделитель списка имён столбцов. «;» — если уже есть в тексте, либо одно имя совпадает с known и содержит запятую (для будущих добавлений). """ s = unicode(text or "").strip() if s == "": return u"," if u";" in s: return u";" if known_names is not None: s_cf = s.casefold() for name in known_names or (): nm = unicode(name or "").strip() if nm != "" and nm.casefold() == s_cf and u"," in nm: return u";" return u"," def split_column_name_tokens(text, known_names=None): """ Список имён столбцов из строки: «;» если есть точка с запятой, иначе «,». known_names: если вся строка = одно известное имя (с запятой) — не дробить. Поддержка экранирования: \\, и \\; """ s = unicode(text or "").strip() if s == "": return [] if u";" in s: return _split_column_name_tokens_by_sep(s, u";") if known_names is not None: s_cf = s.casefold() for name in known_names or (): nm = unicode(name or "").strip() if nm != "" and nm.casefold() == s_cf: return [nm] return _split_column_name_tokens_by_sep(s, u",") def join_column_name_tokens(tokens, sep=None): """Склеить имена. sep=None → «;» если в любом есть запятая, иначе «,».""" parts = [] for t in tokens or []: s = unicode(t or u"").strip() if s != "": parts.append(s) if not parts: return u"" if sep not in (u",", u";"): sep = u";" if any(u"," in p for p in parts) else u"," elif sep == u"," and any(u"," in p for p in parts): sep = u";" return sep.join(parts) def append_column_name_token(text, new_token, sep=None, known_names=None): """ Добавить имя столбца к списку без дублей. Если новое или уже выбранное имя содержит «,» — разделитель «;» (уже накопленные «,» между токенами пересобираются в «;»). Возвращает (joined_text, effective_sep). """ token = unicode(new_token or u"").strip() raw = unicode(text or u"").strip() cur_sep = sep if sep in (u",", u";") else detect_column_name_list_sep(raw, known_names) if token == u"": return (raw, cur_sep) token_cf = token.casefold() if token_cf in (u"all", u"все", u"*"): return (u"Все", cur_sep) tokens = split_column_name_tokens(raw, known_names) if len(tokens) == 1 and tokens[0].casefold() in (u"all", u"все", u"*"): tokens = [] for t in tokens: if unicode(t).strip().casefold() == token_cf: return (join_column_name_tokens(tokens, sep=cur_sep), cur_sep) if u"," in token or any(u"," in unicode(t) for t in tokens): cur_sep = u";" tokens.append(token) return (join_column_name_tokens(tokens, sep=cur_sep), cur_sep) def expand_rename_columns_mapping_pairs(mappings): """ Развернуть mappings: old/new могут содержать несколько имён через , или ;. Пары сопоставляются по позиции (1-е → 1-е, …). """ out = [] for item in mappings or []: if not isinstance(item, dict): continue olds = split_column_name_tokens( item.get("old") or item.get("from") or item.get("source") or "" ) news = split_column_name_tokens( item.get("new") or item.get("to") or item.get("dest") or "" ) if not olds or not news: continue n = min(len(olds), len(news)) i = 0 while i < n: o = olds[i] nv = news[i] if o != "" and nv != "": out.append({"old": o, "new": nv}) i = i + 1 return out def _parse_column_rename_mappings(value): if value is None: return [] if isinstance(value, (list, tuple)): out = [] for item in value: if isinstance(item, dict): old = unicode( item.get("old") or item.get("from") or item.get("source") or "" ).strip() new = unicode( item.get("new") or item.get("to") or item.get("dest") or "" ).strip() if old != "" and new != "": out.append({"old": old, "new": new}) elif isinstance(item, (list, tuple)) and len(item) >= 2: old = unicode(item[0]).strip() new = unicode(item[1]).strip() if old != "" and new != "": out.append({"old": old, "new": new}) return out text = unicode(value).strip() if text == "": return [] out = [] pair_sep = u";" if u";" in text and u"=" in text else u"," for part in re.split(re.escape(pair_sep), text): part = part.strip() if part == "": continue if "=" in part: old, _, new = part.partition("=") elif "->" in part: old, _, new = part.partition("->") else: continue old = old.strip() new = new.strip() if old != "" and new != "": out.append({"old": old, "new": new}) return out def _normalize_rename_columns_block(block): out = _normalize_block("переименовать_столбцы", block) mappings = block.get("mappings") if mappings is None: mappings = block.get("map") or block.get("renames") if mappings is None: old_name = unicode(block.get("old") or block.get("from") or "").strip() new_name = unicode(block.get("new") or block.get("to") or "").strip() if old_name != "" and new_name != "": mappings = [{"old": old_name, "new": new_name}] parsed = _parse_column_rename_mappings(mappings) if parsed: out["mappings"] = parsed else: out.pop("mappings", None) out.pop("old", None) out.pop("new", None) out.pop("from", None) out.pop("to", None) return _attach_sheet(out, out.get("sheet")) def _normalize_reorder_columns_block(block): out = _normalize_block("переставить_столбцы", block) out["columns"] = _normalize_column_token_list(out.get("columns")) pos = _normalize_reorder_position(out.get("position")) if pos != "": out["position"] = pos else: out.pop("position", None) rel = unicode( out.get("relative_column") or out.get("relative") or out.get("anchor") or "" ).strip() if rel != "": out["relative_column"] = rel else: out.pop("relative_column", None) out.pop("relative", None) out.pop("anchor", None) do_copy = False if "copy" in out: do_copy = bool(out.get("copy")) else: mode = unicode(out.get("mode") or "").strip().casefold() if mode in ( "copy", "копировать", "_копировать_", "дублировать", "duplicate", ): do_copy = True out.pop("mode", None) if do_copy: out["copy"] = True else: out.pop("copy", None) raw_names = out.get("new_names") if raw_names is None: raw_names = out.get("copy_names") if raw_names is None: raw_names = out.get("new_columns") names = [] if isinstance(raw_names, (list, tuple)): for item in raw_names: names.append(unicode("" if item is None else item).strip()) elif raw_names is not None and unicode(raw_names).strip() != "": for part in re.split(r"[,;]", unicode(raw_names)): names.append(part.strip()) out.pop("copy_names", None) out.pop("new_columns", None) if do_copy and names: out["new_names"] = names else: out.pop("new_names", None) sheet = unicode(out.get("sheet") or "").strip() if sheet != "": out["sheet"] = sheet elif "sheet" in out: del out["sheet"] return out def _normalize_value_count_block(block): out = _normalize_block("количество_значений", block) col = unicode(out.get("new_column") or out.get("column") or u"").strip() if col != u"": out["new_column"] = col else: out.pop("new_column", None) out.pop("column", None) cols = out.get("key_columns") if cols is None: cols = out.get("columns") out["key_columns"] = _normalize_column_token_list(cols) out.pop("columns", None) if "trim" in out: out["trim"] = bool(out.get("trim")) else: out["trim"] = True if "case_sensitive" in out: out["case_sensitive"] = bool(out.get("case_sensitive")) else: out["case_sensitive"] = False return _attach_sheet(out, out.get("sheet")) def _normalize_copy_sheet_block(block): out = _normalize_block("копировать_лист", block) out["source_sheet"] = unicode( out.get("source_sheet") or out.get("source") or u"" ).strip() out["dest_sheet"] = unicode( out.get("dest_sheet") or out.get("dest") or out.get("target_sheet") or u"" ).strip() cols = out.get("columns") if cols is None: cols = out.get("markers") # совместимость: раньше часто думали “удалить маркеры” out["columns"] = _normalize_column_token_list(cols) out.pop("markers", None) if "skip_empty" in out: out["skip_empty"] = bool(out.get("skip_empty")) sec = out.get("skip_empty_columns") if sec is None: sec = out.get("skip_empty_cols") out["skip_empty_columns"] = _normalize_column_token_list(sec) out.pop("skip_empty_cols", None) if "header_row" in out and out.get("header_row") not in (None, u""): try: out["header_row"] = int(out.get("header_row")) except (TypeError, ValueError): out.pop("header_row", None) out.pop("source", None) out.pop("dest", None) out.pop("target_sheet", None) return _attach_sheet(out, out.get("sheet")) def _normalize_merge_sheets_into_one_block(block): out = _normalize_block("объединить_листы_в_один", block) sheets = out.get("sheets") if sheets is None: sheets = out.get("source_sheets") or out.get("sheet_list") if isinstance(sheets, (list, tuple)): out["sheets"] = [ unicode(x or u"").strip() for x in sheets if unicode(x or u"").strip() != u"" ] elif sheets is not None: out["sheets"] = [ p.strip() for p in unicode(sheets).replace(u";", u",").split(u",") if p.strip() != u"" ] else: out["sheets"] = [] out.pop("source_sheets", None) out.pop("sheet_list", None) out["dest_sheet"] = unicode( out.get("dest_sheet") or out.get("result_sheet") or out.get("target_sheet") or out.get("dest") or u"" ).strip() out.pop("result_sheet", None) out.pop("target_sheet", None) out.pop("dest", None) if "header_row" in out and out.get("header_row") not in (None, u""): try: out["header_row"] = int(out.get("header_row")) except (TypeError, ValueError): out.pop("header_row", None) # with_formatting: копировать визуальное оформление ячеек с источников wf = None for key in ( "with_formatting", "с_форматированием", "keep_format", "copy_format", "keep_formatting", ): if key in out: wf = out.get(key) break if wf is None: out["with_formatting"] = False else: try: from libre_macros_lib import lm_parse_bool_param out["with_formatting"] = bool(lm_parse_bool_param(wf, default=False)) except Exception: out["with_formatting"] = bool(wf) for key in ( "с_форматированием", "keep_format", "copy_format", "keep_formatting", ): out.pop(key, None) # columns: пусто = все столбцы по заголовкам источников cols = _normalize_column_token_list(out.get("columns")) if cols: out["columns"] = cols else: out.pop("columns", None) # auto_format: оформление заголовка/шрифта данных по умолчанию af = None for key in ("auto_format", "автоформат", "autofmt", "default_format"): if key in out: af = out.get(key) break if af is None: out["auto_format"] = True else: try: from libre_macros_lib import lm_parse_bool_param out["auto_format"] = bool(lm_parse_bool_param(af, default=True)) except Exception: out["auto_format"] = bool(af) for key in ("автоформат", "autofmt", "default_format"): out.pop(key, None) return _attach_sheet(out, out.get("sheet")) _SPLIT_SHEETS_RUN_PHASES = ( "after_collect", "after_postprocess", "after_final", ) def _normalize_split_sheets_block(block): out = _normalize_block("разделить_листы", block) phase = unicode(out.get("run_phase") or u"after_postprocess").strip().casefold() if phase not in _SPLIT_SHEETS_RUN_PHASES: phase = u"after_postprocess" out["run_phase"] = phase src = out.get("source") if not isinstance(src, dict): src = {} out["source"] = { "book": u"эта_книга", "sheet": unicode( src.get("sheet") or out.get("source_sheet") or out.get("sheet") or u"" ).strip(), } out.pop("source_sheet", None) def _norm_fields(key): raw = out.get(key) if raw is None: out[key] = [] return if isinstance(raw, (list, tuple)): out[key] = [ unicode(x or u"").strip() for x in raw if unicode(x or u"").strip() != u"" ] else: out[key] = [ p.strip() for p in unicode(raw).replace(u";", u",").split(u",") if p.strip() != u"" ] _norm_fields("split_books") _norm_fields("split_sheets") out["book_template"] = unicode( out.get("book_template") or out.get("book") or u"эта_книга" ).strip() out["sheet_template"] = unicode( out.get("sheet_template") or out.get("sheet") or u"[Split]" ).strip() mode = unicode(out.get("split_mode") or u"copy_range").strip().casefold() out["split_mode"] = mode if mode in (u"copy_range", u"copy_sheet") else u"copy_range" if "header_row" in out and out.get("header_row") not in (None, u""): try: out["header_row"] = int(out.get("header_row")) except (TypeError, ValueError): out.pop("header_row", None) for key, default in ( ("pre_sort", True), ("copy_header", True), ("create_folders", True), ("overwrite_sheets", True), ("append_if_exists", False), ("save_immediately", False), ("paste_formats", False), ): if key not in out: out[key] = default else: try: from libre_macros_lib import lm_parse_bool_param out[key] = bool(lm_parse_bool_param(out.get(key), default=default)) except Exception: out[key] = bool(out.get(key)) return out def _column_tokens_to_row_extra(cols): """Список столбцов из блока → extra_args для lm_pp_row_convert_numeric_strings.""" out = [] for c in cols or []: if isinstance(c, int): out.append(c) continue if isinstance(c, float): out.append(c) continue t = unicode(c).strip() if t == "": continue if t.isdigit(): out.append(int(t)) continue try: if "." in t: out.append(float(t.replace(",", "."))) continue except (TypeError, ValueError): pass out.append(t) return out def param_decode(fn_key, raw_text): """Всегда list[dict] — нормализованные блоки. Только JSON.""" fn_key = normalize_fn_key(fn_key) raw = unicode(raw_text or "").strip() if raw == "": return [] if len(raw) >= 2 and raw[0] == u""" and raw[-1] == u""": unq_s = unicode(raw[1:-1]).strip() if _looks_like_json_payload(unq_s): raw = unq_s if not _looks_like_json_payload(raw): return [] try: items = _parse_json_root(raw) except (ValueError, TypeError): return [] return _decode_json_blocks(fn_key, items) def param_encode(fn_key, blocks): """Сериализация блоков в компактный JSON.""" fn_key = normalize_fn_key(fn_key) blocks = blocks or [] payload = [] for block in blocks: b = _normalize_block_for_fn(fn_key, block) payload.append(b) return json.dumps(payload, ensure_ascii=False, separators=(",", ":")) def row_extra_args_from_codec(fn_key, raw_text): """ Блоки кодека → extra_args для ROW-исполнителей. Возвращает list или None, если fn_key не ROW-кодек. """ fn_key = normalize_fn_key(fn_key) if fn_key not in ("преобразовать_числа", "цвет_текста_по_значению"): return None blocks = param_decode(fn_key, raw_text) if fn_key == "цвет_текста_по_значению": if len(blocks) == 0: return [] if len(blocks) == 1: m = unicode(blocks[0].get("marker") or u"статус").strip() return [m or u"статус"] return [param_encode(fn_key, blocks)] if len(blocks) == 0: return [] if len(blocks) == 1: cols = blocks[0].get("columns") or [] if not cols: return [] return _column_tokens_to_row_extra(cols) return [param_encode(fn_key, blocks)]