""" Generates src/fields.ts from an MPXJ checkout: for tasks, resources and assignments, the field number Microsoft Project stores in an .mpp file's field map, mapped to MPXJ's field name, data type and (for durations) the field that holds its units. Sources (MPXJ, LGPL-2.1): src/main/java/org/mpxj/{Task,Resource,Assignment}Field.java and src/main/java/org/mpxj/common/MPP{Task,Resource,Assignment}Field.java, including MPPTaskField.mapMpp14 (fields whose numbers moved in Project 2010). Run: python tools/gen_fields.py """ import re import sys from pathlib import Path root = Path(sys.argv[1]) / "src/main/java/org/mpxj" out = Path(__file__).resolve().parent.parent / "src/fields.ts" def enum(kind): text = (root / f"{kind}Field.java").read_text(encoding="utf-8") fields = {} for m in re.finditer(r"^ ([A-Z0-9_]+)\(DataType\.([A-Z_]+)(?:,\s*(?:[A-Za-z]+Field\.)?([A-Z0-9_]+))?\)", text, re.M): fields[m.group(1)] = (m.group(2), m.group(3)) return fields def mpp_map(kind): text = (root / f"common/MPP{kind}Field.java").read_text(encoding="utf-8") numbers = {} for m in re.finditer(r"FIELD_ARRAY\[(\d+)\] = " + kind + r"Field\.([A-Z0-9_]+);", text): numbers[int(m.group(1))] = m.group(2) alternate = {} for m in re.finditer(r"ALTERNATE_FIELD_ARRAY\[(\d+)\] = " + kind + r"Field\.([A-Z0-9_]+);", text): alternate[int(m.group(1))] = m.group(2) mpp14 = {} body = re.search(r"private static FieldType mapMpp14\(int value\)(.*?)\n }", text, re.S) if body: for m in re.finditer(r"case (\d+):\s*\{\s*result = " + kind + r"Field\.([A-Z0-9_]+);", body.group(1)): mpp14[int(m.group(1))] = m.group(2) return numbers, alternate, mpp14 lines = [ "// Generated by tools/gen_fields.py from MPXJ (LGPL-2.1); do not edit by hand.", "// [name, data type, units field name or ''] by the field number in an .mpp field map.", "", "export type FieldDef = readonly [name: string, dataType: string, units: string];", "", ] for kind in ["Task", "Resource", "Assignment"]: types = enum(kind) numbers, alternate, mpp14 = mpp_map(kind) def row(name): dt, units = types.get(name, ("CUSTOM", None)) return f'["{name}", "{dt}", "{units or ""}"]' lines.append(f"export const {kind.upper()}_FIELDS: Record = {{") for n in sorted(numbers): lines.append(f" {n}: {row(numbers[n])},") lines.append("};") if mpp14: lines.append(f"/** Project 2010 and later store these {kind.lower()} fields under other numbers. */") lines.append(f"export const {kind.upper()}_FIELDS_MPP14: Record = {{") for n in sorted(mpp14): lines.append(f" {n}: {row(mpp14[n])},") lines.append("};") if alternate: lines.append(f"/** Numbers in the 0x4000 range some files use for {kind.lower()} custom fields. */") lines.append(f"export const {kind.upper()}_FIELDS_ALTERNATE: Record = {{") for n in sorted(alternate): lines.append(f" {n}: {row(alternate[n])},") lines.append("};") lines.append("") # The units fields a duration names must themselves be readable, so check they exist. missing = {u for n, (d, u) in types.items() if u and u not in types} if missing: raise SystemExit(f"{kind}: units fields not found: {sorted(missing)[:5]}") out.write_text("\n".join(lines), encoding="utf-8", newline="\n") print(f"wrote {out} ({out.stat().st_size // 1024} KB)")