Files
2026-06-04 09:28:01 +00:00

446 lines
15 KiB
Python

#!/usr/bin/env python3
# mxl-info v1.0 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C spreadsheet (MXL) structure", allow_abbrev=False)
parser.add_argument("-TemplatePath", "-Path", default="", help="Path to Template.xml")
parser.add_argument("-ProcessorName", default="", help="Processor name (used with -TemplateName)")
parser.add_argument("-TemplateName", default="", help="Template name (used with -ProcessorName)")
parser.add_argument("-SrcDir", default="src", help="Source directory (default: src)")
parser.add_argument("-Format", choices=["text", "json"], default="text", help="Output format")
parser.add_argument("-WithText", action="store_true", default=False, help="Include text content")
parser.add_argument("-MaxParams", type=int, default=10, help="Max parameters to show per area")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
args = parser.parse_args()
# --- Resolve template path ---
template_path = args.TemplatePath
if not template_path:
if not args.ProcessorName or not args.TemplateName:
print("Specify -TemplatePath or both -ProcessorName and -TemplateName", file=sys.stderr)
sys.exit(1)
template_path = os.path.join(args.SrcDir, args.ProcessorName, "Templates", args.TemplateName, "Ext", "Template.xml")
if not os.path.isabs(template_path):
template_path = os.path.join(os.getcwd(), template_path)
if not os.path.isfile(template_path):
print(f"File not found: {template_path}", file=sys.stderr)
sys.exit(1)
# --- Load XML ---
tree = etree.parse(template_path, etree.XMLParser(remove_blank_text=True))
root = tree.getroot()
NS = {
"d": "http://v8.1c.ru/8.2/data/spreadsheet",
"v8": "http://v8.1c.ru/8.1/data/core",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
# --- Column sets ---
column_sets = []
default_col_count = 0
for cols in root.findall("d:columns", NS):
size_node = cols.find("d:size", NS)
id_node = cols.find("d:id", NS)
size = int(size_node.text) if size_node is not None and size_node.text else 0
if id_node is not None:
column_sets.append({"Id": id_node.text or "", "Size": size})
else:
default_col_count = size
# --- Rows: collect row data ---
row_nodes = root.findall("d:rowsItem", NS)
total_rows = len(row_nodes)
height_node = root.find("d:height", NS)
doc_height = int(height_node.text) if height_node is not None and height_node.text else total_rows
# --- Named items ---
named_areas = []
named_drawings = []
for ni in root.findall("d:namedItem", NS):
ni_type = ni.get(f"{{{XSI_NS}}}type", "")
name_node = ni.find("d:name", NS)
name = name_node.text if name_node is not None else ""
if "NamedItemCells" in ni_type:
area = ni.find("d:area", NS)
area_type_node = area.find("d:type", NS)
area_type = area_type_node.text if area_type_node is not None else ""
begin_row = int(area.find("d:beginRow", NS).text)
end_row = int(area.find("d:endRow", NS).text)
begin_col = int(area.find("d:beginColumn", NS).text)
end_col = int(area.find("d:endColumn", NS).text)
cols_id = None
cols_id_node = area.find("d:columnsID", NS)
if cols_id_node is not None:
cols_id = cols_id_node.text
named_areas.append({
"Name": name,
"AreaType": area_type,
"BeginRow": begin_row,
"EndRow": end_row,
"BeginCol": begin_col,
"EndCol": end_col,
"ColumnsID": cols_id,
})
elif "NamedItemDrawing" in ni_type:
draw_id_node = ni.find("d:drawingID", NS)
draw_id = draw_id_node.text if draw_id_node is not None else ""
named_drawings.append({"Name": name, "DrawingID": draw_id})
# --- Scan rows for parameters and text ---
# Build row index map: rowIndex -> XmlNode
row_map = {}
for ri in row_nodes:
idx_node = ri.find("d:index", NS)
if idx_node is not None and idx_node.text:
idx = int(idx_node.text)
row_map[idx] = ri
def get_cell_data(row_node, include_text):
row = row_node.find("d:row", NS)
if row is None:
return []
results = []
for c_group in row.findall("d:c", NS):
cell = c_group.find("d:c", NS)
if cell is None:
continue
param = cell.find("d:parameter", NS)
detail = cell.find("d:detailParameter", NS)
tl = cell.find("d:tl", NS)
if param is not None:
entry = {"Kind": "Parameter", "Value": param.text or ""}
if detail is not None:
entry["Detail"] = detail.text or ""
results.append(entry)
if tl is not None:
content = tl.find("v8:item/v8:content", NS)
if content is not None and content.text:
text = content.text
is_template = bool(re.search(r'\[.+\]', text))
if is_template:
# Extract parameter names from [Param] placeholders
# Skip numeric-only like [5]
for m in re.finditer(r'\[([^\]]+)\]', text):
val = m.group(1)
if not re.match(r'^\d+$', val):
results.append({"Kind": "TemplateParam", "Value": val})
# Full template text only with -WithText
if include_text:
results.append({"Kind": "Template", "Value": text})
elif include_text:
results.append({"Kind": "Text", "Value": text})
return results
def get_area_cell_data(area, row_map_ref, include_text):
params = []
details = []
texts = []
templates = []
start_row = area["BeginRow"]
end_row = area["EndRow"]
if start_row == -1:
start_row = 0
if end_row == -1:
end_row = doc_height - 1
for r in range(start_row, end_row + 1):
if r in row_map_ref:
cells = get_cell_data(row_map_ref[r], include_text)
for c in cells:
kind = c["Kind"]
if kind == "Parameter":
params.append(c["Value"])
if "Detail" in c:
details.append(f"{c['Value']}->{c['Detail']}")
elif kind == "TemplateParam":
params.append(f"{c['Value']} [tpl]")
elif kind == "Text":
texts.append(c["Value"])
elif kind == "Template":
templates.append(c["Value"])
return {"Params": params, "Details": details, "Texts": texts, "Templates": templates}
# Sort areas by position: Rows by beginRow, Columns by beginCol, Rectangle by beginRow
def area_sort_key(a):
if a["AreaType"] == "Columns":
return (a["BeginCol"], a["Name"])
return (a["BeginRow"], a["Name"])
named_areas.sort(key=area_sort_key)
# Collect data for each area
area_data = []
covered_rows = set()
for area in named_areas:
data = get_area_cell_data(area, row_map, args.WithText)
area_data.append({
"Area": area,
"Params": data["Params"],
"Details": data["Details"],
"Texts": data["Texts"],
"Templates": data["Templates"],
})
# Track covered rows
sr = area["BeginRow"]
er = area["EndRow"]
if sr != -1 and er != -1:
for r in range(sr, er + 1):
covered_rows.add(r)
# Find parameters outside named areas
outside_params = []
outside_details = []
outside_texts = []
outside_templates = []
for r in sorted(row_map.keys()):
if r not in covered_rows:
cells = get_cell_data(row_map[r], args.WithText)
for c in cells:
kind = c["Kind"]
if kind == "Parameter":
outside_params.append(c["Value"])
if "Detail" in c:
outside_details.append(f"{c['Value']}->{c['Detail']}")
elif kind == "TemplateParam":
outside_params.append(f"{c['Value']} [tpl]")
elif kind == "Text":
outside_texts.append(c["Value"])
elif kind == "Template":
outside_templates.append(c["Value"])
# --- Counts ---
merge_count = len(root.findall("d:merge", NS))
drawing_nodes = root.findall("d:drawing", NS)
drawing_count = len(drawing_nodes)
# --- Output ---
def truncate_list(items, max_count):
if len(items) <= max_count:
return ", ".join(items)
shown = ", ".join(items[:max_count])
remaining = len(items) - max_count
return f"{shown}, ... (+{remaining})"
# Determine template name from path
template_name = os.path.basename(os.path.dirname(os.path.dirname(template_path)))
if args.Format == "json":
result = {
"name": template_name,
"rows": doc_height,
"columns": default_col_count,
"columnSets": [{"id": cs["Id"], "size": cs["Size"]} for cs in column_sets],
"areas": [],
"outsideParams": list(outside_params),
"mergeCount": merge_count,
"drawingCount": drawing_count,
}
for ad in area_data:
area_obj = {
"name": ad["Area"]["Name"],
"type": ad["Area"]["AreaType"],
"beginRow": ad["Area"]["BeginRow"],
"endRow": ad["Area"]["EndRow"],
"beginCol": ad["Area"]["BeginCol"],
"endCol": ad["Area"]["EndCol"],
"params": list(ad["Params"]),
}
if ad["Area"]["ColumnsID"]:
area_obj["columnsID"] = ad["Area"]["ColumnsID"]
if args.WithText:
area_obj["texts"] = list(ad["Texts"])
area_obj["templates"] = list(ad["Templates"])
result["areas"].append(area_obj)
if args.WithText:
result["outsideTexts"] = list(outside_texts)
result["outsideTemplates"] = list(outside_templates)
for nd in named_drawings:
result["areas"].append({
"name": nd["Name"],
"type": "Drawing",
"drawingID": nd["DrawingID"],
})
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(0)
# --- Text format output ---
lines = []
lines.append(f"=== {template_name} ===")
lines.append(f" Rows: {doc_height}, Columns: {default_col_count}")
if len(column_sets) == 0:
lines.append(" Column sets: 1 (default only)")
else:
lines.append(f" Column sets: {len(column_sets) + 1} (default={default_col_count} cols + {len(column_sets)} additional)")
for cs in column_sets:
lines.append(f" {cs['Id'][:8]}...: {cs['Size']} cols")
lines.append("")
lines.append("--- Named areas ---")
for ad in area_data:
a = ad["Area"]
param_count = len(ad["Params"])
row_range = ""
if a["AreaType"] == "Rows":
row_range = f"rows {a['BeginRow']}-{a['EndRow']}"
elif a["AreaType"] == "Columns":
row_range = f"cols {a['BeginCol']}-{a['EndCol']}"
elif a["AreaType"] == "Rectangle":
row_range = f"rows {a['BeginRow']}-{a['EndRow']}, cols {a['BeginCol']}-{a['EndCol']}"
cols_info = ""
if a["ColumnsID"]:
cs_size = ""
for cs in column_sets:
if cs["Id"] == a["ColumnsID"]:
cs_size = f" {cs['Size']}cols"
break
cols_info = f" [colset{cs_size}]"
param_info = f"({param_count} params)"
name_str = a["Name"].ljust(25)
type_str = a["AreaType"].ljust(12)
lines.append(f" {name_str} {type_str} {row_range} {param_info}{cols_info}")
for nd in named_drawings:
name_str = nd["Name"].ljust(25)
lines.append(f" {name_str} Drawing drawingID={nd['DrawingID']}")
# Detect intersection pairs (Rows + Columns areas that overlap)
rows_areas = [ad for ad in area_data if ad["Area"]["AreaType"] == "Rows"]
cols_areas = [ad for ad in area_data if ad["Area"]["AreaType"] == "Columns"]
intersections = []
if rows_areas and cols_areas:
for ra in rows_areas:
for ca in cols_areas:
intersections.append(f"{ra['Area']['Name']}|{ca['Area']['Name']}")
if intersections:
lines.append("")
lines.append("--- Intersections (use with GetArea) ---")
for pair in intersections:
lines.append(f" {pair}")
# Parameters by area
has_params = any(len(ad["Params"]) > 0 for ad in area_data) or len(outside_params) > 0
if has_params:
lines.append("")
lines.append("--- Parameters by area ---")
for ad in area_data:
if len(ad["Params"]) > 0:
param_str = truncate_list(ad["Params"], args.MaxParams)
lines.append(f" {ad['Area']['Name']}: {param_str}")
# Show detailParameters if any
if len(ad["Details"]) > 0:
detail_str = truncate_list(ad["Details"], args.MaxParams)
lines.append(f" detail: {detail_str}")
if len(outside_params) > 0:
param_str = truncate_list(outside_params, args.MaxParams)
lines.append(f" (outside areas): {param_str}")
if len(outside_details) > 0:
detail_str = truncate_list(outside_details, args.MaxParams)
lines.append(f" detail: {detail_str}")
# WithText sections
if args.WithText:
has_text = any(len(ad["Texts"]) > 0 or len(ad["Templates"]) > 0 for ad in area_data) or len(outside_texts) > 0 or len(outside_templates) > 0
if has_text:
lines.append("")
lines.append("--- Text content ---")
for ad in area_data:
if len(ad["Texts"]) > 0 or len(ad["Templates"]) > 0:
lines.append(f" {ad['Area']['Name']}:")
if len(ad["Texts"]) > 0:
text_items = [f'"{t}"' for t in ad["Texts"]]
text_str = truncate_list(text_items, args.MaxParams)
lines.append(f" Text: {text_str}")
if len(ad["Templates"]) > 0:
tpl_items = [f'"{t}"' for t in ad["Templates"]]
tpl_str = truncate_list(tpl_items, args.MaxParams)
lines.append(f" Templates: {tpl_str}")
if len(outside_texts) > 0 or len(outside_templates) > 0:
lines.append(" (outside areas):")
if len(outside_texts) > 0:
text_items = [f'"{t}"' for t in outside_texts]
text_str = truncate_list(text_items, args.MaxParams)
lines.append(f" Text: {text_str}")
if len(outside_templates) > 0:
tpl_items = [f'"{t}"' for t in outside_templates]
tpl_str = truncate_list(tpl_items, args.MaxParams)
lines.append(f" Templates: {tpl_str}")
lines.append("")
lines.append("--- Stats ---")
lines.append(f" Merges: {merge_count}")
lines.append(f" Drawings: {drawing_count}")
# --- Truncation protection ---
total_lines = len(lines)
if args.Offset > 0:
if args.Offset >= total_lines:
print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.")
sys.exit(0)
lines = lines[args.Offset:]
if len(lines) > args.Limit:
shown = lines[:args.Limit]
for l in shown:
print(l)
remaining = total_lines - args.Offset - args.Limit
print("")
print(f"[TRUNCATED] Shown {args.Limit} of {total_lines} lines. Use -Offset {args.Offset + args.Limit} to continue.")
else:
for l in lines:
print(l)