@dforge-core/dforge-mcp 0.2.45 → 0.2.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +2 -1
- package/dist/server.js +71 -3
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +1 -1
- package/resources/schemas/data-views.schema.json +50 -0
- package/scripts/install-skills.mjs +1 -0
- package/skills/dforge-1c-import/SKILL.md +375 -0
- package/skills/dforge-1c-import/references/environment.md +98 -0
- package/skills/dforge-1c-import/references/loading.md +164 -0
- package/skills/dforge-1c-import/references/measurements.md +210 -0
- package/skills/dforge-1c-import/references/print-forms.md +148 -0
- package/skills/dforge-1c-import/scripts/1c.py +178 -0
- package/skills/dforge-1c-import/scripts/README.md +164 -0
- package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
- package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
- package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
- package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
- package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
- package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
- package/skills/dforge-1c-import/scripts/lib/changes.py +326 -0
- package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
- package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
- package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
- package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
- package/skills/dforge-1c-import/scripts/lib/delta.py +770 -0
- package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/docusage.py +106 -0
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +408 -0
- package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
- package/skills/dforge-1c-import/scripts/lib/external.py +202 -0
- package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
- package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
- package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
- package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
- package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
- package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
- package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
- package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
- package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
- package/skills/dforge-1c-import/scripts/lib/meta.py +325 -0
- package/skills/dforge-1c-import/scripts/lib/migrate.py +1318 -0
- package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
- package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/onecd.py +269 -0
- package/skills/dforge-1c-import/scripts/lib/paths.py +111 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +68 -0
- package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
- package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
- package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
- package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
- package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
- package/skills/dforge-1c-import/scripts/lib/recorders.py +91 -0
- package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
- package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
- package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
- package/skills/dforge-1c-import/scripts/lib/seedstate.py +493 -0
- package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
- package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
- package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -0
- package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
- package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
- package/skills/dforge-1c-import/scripts/lib/store.py +126 -0
- package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
- package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
- package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
- package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
- package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
- package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
- package/skills/dforge-1c-import/scripts/project-template/.env.example +24 -0
- package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +24 -0
- package/skills/dforge-mcp-author/SKILL.md +5 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +1 -1
- package/skills/dforge-mcp-author/references/data-views.md +38 -2
- package/skills/dforge-mcp-author/references/formulas.md +32 -1
- package/skills/dforge-mcp-author/references/print-templates.md +30 -5
- package/skills/dforge-mcp-author/references/queries.md +17 -2
- package/skills/dforge-mcp-author/references/registers.md +1 -0
- package/skills/dforge-mcp-author/references/reports.md +1 -1
- package/skills/dforge-mcp-author/references/validation-checklist.md +1 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
u"""Read the configuration's print templates (макеты, MOXCEL spreadsheets) into HTML and JSON.
|
|
4
|
+
|
|
5
|
+
An object lists its templates as the group `{3daea016-69b7-4ed4-9453-127911372fe6, n, guid...}`
|
|
6
|
+
in its metadata. A template's own `<guid>.txt` names it, and its body is `<guid>.0.txt`. A
|
|
7
|
+
spreadsheet body is `MOXCEL` and a few binary bytes, then a brace list:
|
|
8
|
+
|
|
9
|
+
8, 1, 9, {languages}, {0}, {column sets}, 0, {0,0} x6,
|
|
10
|
+
1, 2, <rows>, then per row: <row>, <format>, <cells>, and per cell <column>, {<cell>}
|
|
11
|
+
{<columns>, 0, <guid>, <count>, then per column <column>, <format>}
|
|
12
|
+
<rows again>, 0 x8,
|
|
13
|
+
{<merges>, {left, top, right, bottom, 0|2} ...}
|
|
14
|
+
{0}, {0},
|
|
15
|
+
{<areas>, name, {1, {kind, left, top, right, bottom, guid}, 0} ...}
|
|
16
|
+
... {<formats>, {mask, values...} ...}, then {<fonts>, {7, ..., height, ..., weight, ...,
|
|
17
|
+
face, ...}}, the cell value formats, the colors
|
|
18
|
+
|
|
19
|
+
A cell is `{0, format}` when it is empty, `{16, format, {1, 1, {lang, text}}}` when it holds
|
|
20
|
+
text or a parameter - lang "" is a parameter, the text its name - and `{20, format, {"U"},
|
|
21
|
+
{1, 1, {lang, text}}}` a template whose `[Name]` are parameters. An area is rows (kind 1, left
|
|
22
|
+
and right -1) or columns (kind 2), inclusive. A cell's format is 1-based, 0 for none; a format's font is 0-based.
|
|
23
|
+
|
|
24
|
+
A format is a bit mask and its values in the order of the bits, low first. What each bit means
|
|
25
|
+
was read off the client's templates: font index (1); the left, top, right and bottom border, a
|
|
26
|
+
line index (2, 4, 8, 16); height (64) and width (128) in 1C's units. The other bits - alignment
|
|
27
|
+
and text placement among them - are carried by number and not reproduced, nor are colors and
|
|
28
|
+
line styles: the HTML is a layout to author a print form from, not a facsimile.
|
|
29
|
+
|
|
30
|
+
python tools/1c.py mxl list # every template: object, name, kind, size
|
|
31
|
+
python tools/1c.py mxl list --client # the integrator's objects only (мсб*, рз*)
|
|
32
|
+
python tools/1c.py mxl dump <object> [<name>] # to out/templates/<object>/<name>.html, .json
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import print_function
|
|
35
|
+
|
|
36
|
+
import codecs
|
|
37
|
+
import html
|
|
38
|
+
import io
|
|
39
|
+
import json
|
|
40
|
+
import os
|
|
41
|
+
import re
|
|
42
|
+
import sys
|
|
43
|
+
|
|
44
|
+
import meta
|
|
45
|
+
import binding
|
|
46
|
+
import paths
|
|
47
|
+
|
|
48
|
+
TEMPLATES = "3daea016-69b7-4ed4-9453-127911372fe6"
|
|
49
|
+
CLIENT = tuple(binding.client_prefixes())
|
|
50
|
+
OUT = os.path.join(paths.OUT, "templates")
|
|
51
|
+
|
|
52
|
+
# a format's bits, in the order their values follow the mask; the unnamed ones are kept by number
|
|
53
|
+
NAMED = {1: "font", 2: "left", 4: "top", 8: "right", 16: "bottom", 64: "height", 128: "width"}
|
|
54
|
+
BITS = [(1 << k, NAMED.get(1 << k, "b%d" % (1 << k))) for k in range(32)]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def objects():
|
|
58
|
+
with codecs.open(os.path.join(paths.OUT, "objects_all.json"), "r", "utf-8") as f:
|
|
59
|
+
return [o for o in json.load(f) if o.get("name")]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def templates_of(guid):
|
|
63
|
+
"""[(template guid, name, synonym)] an object lists."""
|
|
64
|
+
text = meta.load(guid)
|
|
65
|
+
if text is None:
|
|
66
|
+
return []
|
|
67
|
+
found = []
|
|
68
|
+
for group in meta.collect_guid_groups(meta.parse(text)):
|
|
69
|
+
if group["classGuid"] != TEMPLATES:
|
|
70
|
+
continue
|
|
71
|
+
for member in group["members"]:
|
|
72
|
+
head = meta.load(member)
|
|
73
|
+
if head is None:
|
|
74
|
+
continue
|
|
75
|
+
name, syn = meta.object_header(head)
|
|
76
|
+
found.append((member, name, syn.get("ru") or syn.get("uk") or ""))
|
|
77
|
+
return found
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def body(guid):
|
|
81
|
+
"""The template's body as bytes, or None."""
|
|
82
|
+
for suffix in (".0.txt", ".0.bin", ".0"):
|
|
83
|
+
path = os.path.join(meta.CONFIG, guid + suffix)
|
|
84
|
+
if os.path.isfile(path):
|
|
85
|
+
with open(path, "rb") as f:
|
|
86
|
+
return f.read()
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def kind(data):
|
|
91
|
+
if data is None:
|
|
92
|
+
return "missing"
|
|
93
|
+
if data.startswith(b"MOXCEL"):
|
|
94
|
+
return "mxl"
|
|
95
|
+
return "other"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def inventory(client_only=False):
|
|
99
|
+
"""[(object family, object name, template guid, template name, synonym, kind, size)]."""
|
|
100
|
+
seen = set()
|
|
101
|
+
out = []
|
|
102
|
+
for o in objects():
|
|
103
|
+
if o["guid"] in seen or (client_only and not (o["name"] or "").startswith(CLIENT)):
|
|
104
|
+
continue
|
|
105
|
+
seen.add(o["guid"])
|
|
106
|
+
for guid, name, syn in templates_of(o["guid"]):
|
|
107
|
+
data = body(guid)
|
|
108
|
+
out.append((o.get("family") or "", o["name"], guid, name, syn, kind(data), len(data or b"")))
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# --------------------------------------------------------------------------- the spreadsheet
|
|
113
|
+
|
|
114
|
+
def tree(data):
|
|
115
|
+
at = data.find(b"{")
|
|
116
|
+
return meta.parse(data[at:].decode("utf-8", "replace"))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _int(value, default=0):
|
|
120
|
+
try:
|
|
121
|
+
return int(value)
|
|
122
|
+
except (TypeError, ValueError):
|
|
123
|
+
return default
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _format(entry):
|
|
127
|
+
"""{property: value} of a format entry {mask, values...}."""
|
|
128
|
+
mask = _int(entry[0])
|
|
129
|
+
values = entry[1:]
|
|
130
|
+
out = {}
|
|
131
|
+
k = 0
|
|
132
|
+
for bit, name in BITS:
|
|
133
|
+
if mask & bit:
|
|
134
|
+
out[name] = values[k] if k < len(values) else None
|
|
135
|
+
k += 1
|
|
136
|
+
return out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _font(entry):
|
|
140
|
+
# {7, 0, ?, height, 0, 0, 0, weight, italic, underline, strike, charset, ..., face, ...}
|
|
141
|
+
at = lambda k: entry[k] if k < len(entry) else None
|
|
142
|
+
return {"size": _int(at(3)) / 10.0, "bold": _int(at(7)) >= 600,
|
|
143
|
+
"italic": _int(at(8)) != 0, "underline": _int(at(9)) != 0,
|
|
144
|
+
"face": next((x for x in entry if isinstance(x, str) and x and not x.lstrip("-").isdigit()), "")}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _text(cell):
|
|
148
|
+
"""(kind, text) of a cell: 'empty', 'text', 'param' or 'template'."""
|
|
149
|
+
tag = _int(cell[0])
|
|
150
|
+
strings = [n for n in meta.walk(cell) if len(n) == 2 and all(isinstance(x, str) for x in n)
|
|
151
|
+
and n[0] in ("", "ru", "uk", "en")]
|
|
152
|
+
value = strings[-1][1] if strings else ""
|
|
153
|
+
lang = strings[-1][0] if strings else None
|
|
154
|
+
if tag == 0 or not strings:
|
|
155
|
+
return "empty", ""
|
|
156
|
+
if tag == 20 or (lang != "" and re.search(r"\[[^\]]+\]", value)):
|
|
157
|
+
# a text cell naming [Parameter] is filled as a template: the code sets that parameter
|
|
158
|
+
# (the transfer's "Оператор: [ОтпускРазрешил]" is a text cell, tag 16)
|
|
159
|
+
return "template", value
|
|
160
|
+
return ("param" if lang == "" else "text"), value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def read(data):
|
|
164
|
+
"""The spreadsheet as {columns: {col: format}, rows: [{row, format, cells: {col: (kind,
|
|
165
|
+
text, format)}}], merges, areas, formats, fonts}."""
|
|
166
|
+
r = tree(data)
|
|
167
|
+
i = 15
|
|
168
|
+
rows = []
|
|
169
|
+
for _ in range(_int(r[i])):
|
|
170
|
+
i += 1
|
|
171
|
+
row, fmt, n = _int(r[i]), _int(r[i + 1]), _int(r[i + 2])
|
|
172
|
+
i += 2
|
|
173
|
+
cells = {}
|
|
174
|
+
for _c in range(n):
|
|
175
|
+
col, cell = _int(r[i + 1]), r[i + 2]
|
|
176
|
+
kind_, text = _text(cell)
|
|
177
|
+
cells[col] = (kind_, text, _int(cell[1]))
|
|
178
|
+
i += 2
|
|
179
|
+
rows.append({"row": row, "format": fmt, "cells": cells})
|
|
180
|
+
i += 1
|
|
181
|
+
columns = {}
|
|
182
|
+
col_list = r[i]
|
|
183
|
+
for k in range(4, len(col_list) - 1, 2):
|
|
184
|
+
columns[_int(col_list[k])] = _int(col_list[k + 1])
|
|
185
|
+
lists = [x for x in r[i + 1:] if isinstance(x, list)]
|
|
186
|
+
merges, areas, formats, fonts = [], {}, [], []
|
|
187
|
+
for x in lists:
|
|
188
|
+
if not merges and x and _int(x[0]) == len(x) - 1 and all(isinstance(y, list) and len(y) == 5 for y in x[1:]) and len(x) > 1:
|
|
189
|
+
merges = [tuple(_int(v) for v in y[:4]) for y in x[1:]]
|
|
190
|
+
elif not areas and len(x) > 2 and isinstance(x[1], str) and isinstance(x[2], list) and _int(x[0]) * 2 == len(x) - 1:
|
|
191
|
+
for k in range(1, len(x), 2):
|
|
192
|
+
spec = x[k + 1][1]
|
|
193
|
+
areas[x[k]] = {"kind": "rows" if _int(spec[0]) == 1 else "columns" if _int(spec[0]) == 2 else "cells",
|
|
194
|
+
"left": _int(spec[1]), "top": _int(spec[2]), "right": _int(spec[3]), "bottom": _int(spec[4])}
|
|
195
|
+
# the formats: the count, then that many lists, each a mask and its values
|
|
196
|
+
for k, x in enumerate(r):
|
|
197
|
+
if isinstance(x, str) and x.isdigit() and k + 1 < len(r) and int(x) > 0 and k > i:
|
|
198
|
+
run = r[k + 1:k + 1 + int(x)]
|
|
199
|
+
if len(run) == int(x) and all(isinstance(y, list) and y and str(y[0]).isdigit() for y in run) \
|
|
200
|
+
and all(len(y) == len([b for b, _ in BITS if _int(y[0]) & b]) + 1 for y in run):
|
|
201
|
+
formats = [_format(y) for y in run]
|
|
202
|
+
after = r[k + 1 + int(x)]
|
|
203
|
+
if isinstance(after, str) and after.isdigit():
|
|
204
|
+
fonts = [_font(y) for y in r[k + 2 + int(x):k + 2 + int(x) + int(after)]]
|
|
205
|
+
break
|
|
206
|
+
return {"columns": columns, "rows": rows, "merges": merges, "areas": areas,
|
|
207
|
+
"formats": formats, "fonts": fonts}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def parameters(sheet, area=None):
|
|
211
|
+
"""The parameter names an area (or the whole sheet) fills, in reading order."""
|
|
212
|
+
names = []
|
|
213
|
+
for row in _rows(sheet, area):
|
|
214
|
+
for col in sorted(row["cells"]):
|
|
215
|
+
kind_, text, _ = row["cells"][col]
|
|
216
|
+
found = [text] if kind_ == "param" else re.findall(r"\[([^\]]+)\]", text) if kind_ == "template" else []
|
|
217
|
+
for name in found:
|
|
218
|
+
if name not in names:
|
|
219
|
+
names.append(name)
|
|
220
|
+
return names
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _rows(sheet, area):
|
|
224
|
+
if area is None:
|
|
225
|
+
return sheet["rows"]
|
|
226
|
+
a = sheet["areas"][area]
|
|
227
|
+
if a["kind"] != "rows":
|
|
228
|
+
return sheet["rows"]
|
|
229
|
+
return [row for row in sheet["rows"] if a["top"] <= row["row"] <= a["bottom"]]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# --------------------------------------------------------------------------- HTML
|
|
233
|
+
|
|
234
|
+
def _fmt(sheet, index):
|
|
235
|
+
return sheet["formats"][index - 1] if 0 < index <= len(sheet["formats"]) else {}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _width(sheet, col):
|
|
239
|
+
return _int(_fmt(sheet, sheet["columns"].get(col, 0)).get("width"), 0)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _style(sheet, fmt):
|
|
243
|
+
css = []
|
|
244
|
+
font = _int(fmt.get("font"), -1)
|
|
245
|
+
if 0 <= font < len(sheet["fonts"]) and "font" in fmt:
|
|
246
|
+
f = sheet["fonts"][font]
|
|
247
|
+
if f["bold"]:
|
|
248
|
+
css.append("font-weight:bold")
|
|
249
|
+
if f["italic"]:
|
|
250
|
+
css.append("font-style:italic")
|
|
251
|
+
if f["underline"]:
|
|
252
|
+
css.append("text-decoration:underline")
|
|
253
|
+
if f["size"]:
|
|
254
|
+
css.append("font-size:%gpt" % f["size"])
|
|
255
|
+
for side in ("left", "top", "right", "bottom"):
|
|
256
|
+
if side in fmt:
|
|
257
|
+
css.append("border-%s:1px solid #000" % side)
|
|
258
|
+
return ";".join(css)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def area_html(sheet, area=None, value=None):
|
|
262
|
+
"""An area as an HTML table. `value(kind, text)` renders a cell's content - by default the
|
|
263
|
+
text as is and a parameter as `{{ name }}`."""
|
|
264
|
+
value = value or _liquid
|
|
265
|
+
rows = _rows(sheet, area)
|
|
266
|
+
if not rows:
|
|
267
|
+
return ""
|
|
268
|
+
top, bottom = rows[0]["row"], rows[-1]["row"]
|
|
269
|
+
width = max([max(r["cells"]) for r in rows if r["cells"]] + [max(sheet["columns"] or [0])]) + 1
|
|
270
|
+
spans, covered = {}, set()
|
|
271
|
+
for left, t, right, b in sheet["merges"]:
|
|
272
|
+
if right < 0 or b < 0:
|
|
273
|
+
continue
|
|
274
|
+
if t < top or b > bottom:
|
|
275
|
+
continue
|
|
276
|
+
spans[(t, left)] = (b - t + 1, right - left + 1)
|
|
277
|
+
for rr in range(t, b + 1):
|
|
278
|
+
for cc in range(left, right + 1):
|
|
279
|
+
if (rr, cc) != (t, left):
|
|
280
|
+
covered.add((rr, cc))
|
|
281
|
+
widths = [max(1, _width(sheet, col)) for col in range(width)]
|
|
282
|
+
out = ['<table class="mxl">', "<colgroup>"]
|
|
283
|
+
for w in widths:
|
|
284
|
+
out.append('<col style="width:%.2f%%">' % (100.0 * w / sum(widths)))
|
|
285
|
+
out.append("</colgroup>")
|
|
286
|
+
by_row = dict((r["row"], r) for r in rows)
|
|
287
|
+
for rn in range(top, bottom + 1):
|
|
288
|
+
row = by_row.get(rn, {"cells": {}, "format": 0})
|
|
289
|
+
height = _int(_fmt(sheet, row["format"]).get("height"), 0)
|
|
290
|
+
out.append('<tr%s>' % (' style="height:%dpx"' % (height * 4 / 3) if height else ""))
|
|
291
|
+
for col in range(width):
|
|
292
|
+
if (rn, col) in covered:
|
|
293
|
+
continue
|
|
294
|
+
kind_, text, fmt = row["cells"].get(col, ("empty", "", 0))
|
|
295
|
+
attrs = ""
|
|
296
|
+
if (rn, col) in spans:
|
|
297
|
+
rs, cs = spans[(rn, col)]
|
|
298
|
+
attrs += (' rowspan="%d"' % rs if rs > 1 else "") + (' colspan="%d"' % cs if cs > 1 else "")
|
|
299
|
+
style = _style(sheet, _fmt(sheet, fmt))
|
|
300
|
+
if style:
|
|
301
|
+
attrs += ' style="%s"' % style
|
|
302
|
+
out.append("<td%s>%s</td>" % (attrs, value(kind_, text)))
|
|
303
|
+
out.append("</tr>")
|
|
304
|
+
out.append("</table>")
|
|
305
|
+
return "\n".join(out)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _liquid(kind_, text):
|
|
309
|
+
if kind_ == "param":
|
|
310
|
+
return "{{ %s }}" % text
|
|
311
|
+
if kind_ == "template":
|
|
312
|
+
return re.sub(r"\[([^\]]+)\]", lambda m: "{{ %s }}" % m.group(1), html.escape(text))
|
|
313
|
+
return html.escape(text).replace("\n", "<br>")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def dump(obj_name, template_name=None):
|
|
317
|
+
written = []
|
|
318
|
+
for o in objects():
|
|
319
|
+
if o["name"] != obj_name:
|
|
320
|
+
continue
|
|
321
|
+
for guid, name, syn in templates_of(o["guid"]):
|
|
322
|
+
if template_name and name != template_name:
|
|
323
|
+
continue
|
|
324
|
+
data = body(guid)
|
|
325
|
+
if kind(data) != "mxl":
|
|
326
|
+
continue
|
|
327
|
+
sheet = read(data)
|
|
328
|
+
folder = os.path.join(OUT, obj_name)
|
|
329
|
+
if not os.path.isdir(folder):
|
|
330
|
+
os.makedirs(folder)
|
|
331
|
+
parts = ["<!-- %s.%s: %s -->" % (obj_name, name, syn)]
|
|
332
|
+
for area in sorted(sheet["areas"], key=lambda a: sheet["areas"][a]["top"]):
|
|
333
|
+
if sheet["areas"][area]["kind"] != "rows":
|
|
334
|
+
continue
|
|
335
|
+
parts.append("<h3>%s</h3>" % html.escape(area))
|
|
336
|
+
parts.append(area_html(sheet, area))
|
|
337
|
+
if not any(a["kind"] == "rows" for a in sheet["areas"].values()):
|
|
338
|
+
parts.append(area_html(sheet))
|
|
339
|
+
with io.open(os.path.join(folder, name + ".html"), "w", encoding="utf-8") as f:
|
|
340
|
+
f.write(u"\n".join(parts) + u"\n")
|
|
341
|
+
info = {"object": obj_name, "template": name, "synonym": syn,
|
|
342
|
+
"areas": dict((a, dict(v, parameters=parameters(sheet, a))) for a, v in sheet["areas"].items()),
|
|
343
|
+
"parameters": parameters(sheet)}
|
|
344
|
+
with io.open(os.path.join(folder, name + ".json"), "w", encoding="utf-8") as f:
|
|
345
|
+
f.write(json.dumps(info, ensure_ascii=False, indent="\t") + u"\n")
|
|
346
|
+
written.append(os.path.join(folder, name))
|
|
347
|
+
return written
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main():
|
|
351
|
+
args = sys.argv[1:]
|
|
352
|
+
command = args[0] if args else "list"
|
|
353
|
+
if command == "list":
|
|
354
|
+
rows = inventory("--client" in args)
|
|
355
|
+
for family, obj, guid, name, syn, kind_, size in rows:
|
|
356
|
+
print("%-10s %-45s %-50s %-6s %7d" % (family, obj, name, kind_, size))
|
|
357
|
+
print("%d template(s), %d spreadsheet(s)" % (len(rows), sum(1 for r in rows if r[5] == "mxl")))
|
|
358
|
+
return 0
|
|
359
|
+
if command == "dump" and len(args) >= 2:
|
|
360
|
+
for path in dump(args[1], args[2] if len(args) > 2 else None):
|
|
361
|
+
print(path)
|
|
362
|
+
return 0
|
|
363
|
+
print(__doc__)
|
|
364
|
+
return 2
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
if __name__ == "__main__":
|
|
368
|
+
sys.exit(main())
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
1C names -> dForge codes.
|
|
5
|
+
|
|
6
|
+
`мсбРасходЭнергоресурсов` has to become `utilities_issue`, `КоличествоМест` has to become
|
|
7
|
+
`places_qty`, and 2,620 of these cannot be typed by hand. The rule is: strip the
|
|
8
|
+
integrator's prefix, split the CamelCase compound into words, translate each word through
|
|
9
|
+
`terms.TERMS`, drop the prepositions, and join with underscores.
|
|
10
|
+
|
|
11
|
+
Two things keep the result usable rather than merely mechanical:
|
|
12
|
+
|
|
13
|
+
* unknown words are transliterated, not dropped, and every one is recorded so
|
|
14
|
+
`python tools/naming.py` can print what the dictionary is still missing;
|
|
15
|
+
* codes are shortened and de-duplicated per scope, because Postgres identifiers are
|
|
16
|
+
capped at 63 characters and 1C names run long.
|
|
17
|
+
|
|
18
|
+
python tools/naming.py # coverage report over the whole scope
|
|
19
|
+
python tools/naming.py <1C name> ... # what these particular names become
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import print_function
|
|
22
|
+
|
|
23
|
+
import collections
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
import terms
|
|
28
|
+
import terms_uk
|
|
29
|
+
|
|
30
|
+
import binding
|
|
31
|
+
import paths
|
|
32
|
+
|
|
33
|
+
# The integrator's and the embedded toolkit's prefixes are tags, not words.
|
|
34
|
+
_TAGS = binding.client_prefixes() + binding.toolkit_prefixes()
|
|
35
|
+
PREFIX = re.compile(u"^(%s)" % u"|".join(re.escape(p) for p in _TAGS) if _TAGS else u"(?!)")
|
|
36
|
+
|
|
37
|
+
# Keeps acronym runs together (ГТД, НН, АТТ) instead of splitting them letter by letter.
|
|
38
|
+
WORD = re.compile(u"[А-ЯЁA-Z]+(?![а-яёa-z])|[А-ЯЁA-Z][а-яёa-z]*|[а-яёa-z]+|\\d+")
|
|
39
|
+
|
|
40
|
+
TRANSLIT = {
|
|
41
|
+
u"а": "a", u"б": "b", u"в": "v", u"г": "g", u"д": "d", u"е": "e", u"ё": "e",
|
|
42
|
+
u"ж": "zh", u"з": "z", u"и": "i", u"й": "y", u"к": "k", u"л": "l", u"м": "m",
|
|
43
|
+
u"н": "n", u"о": "o", u"п": "p", u"р": "r", u"с": "s", u"т": "t", u"у": "u",
|
|
44
|
+
u"ф": "f", u"х": "h", u"ц": "ts", u"ч": "ch", u"ш": "sh", u"щ": "sch",
|
|
45
|
+
u"ъ": "", u"ы": "y", u"ь": "", u"э": "e", u"ю": "yu", u"я": "ya",
|
|
46
|
+
u"і": "i", u"ї": "i", u"є": "ie", u"ґ": "g",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Words the dictionary had no entry for, collected so the gap is visible rather than
|
|
50
|
+
# silently transliterated away.
|
|
51
|
+
UNKNOWN = collections.Counter()
|
|
52
|
+
|
|
53
|
+
MAX_LEN = 55 # Postgres caps identifiers at 63; leave room for the "_id" suffix and more
|
|
54
|
+
|
|
55
|
+
# A column named after a Postgres reserved word passes every JSON schema and then fails
|
|
56
|
+
# the module import with a raw `syntax error at or near "order"`, because not every
|
|
57
|
+
# statement dForge generates quotes its identifiers. These are the collisions the port
|
|
58
|
+
# actually hits; the names are the ones already installed, so regenerating does not move
|
|
59
|
+
# a column that exists in the tenant. Anything new falls back to a `_col` suffix, which
|
|
60
|
+
# `validate.check_reserved` will catch if it ever stops being enough.
|
|
61
|
+
RESERVED_RENAME = {
|
|
62
|
+
"order": "order_id",
|
|
63
|
+
"user": "user_id",
|
|
64
|
+
"natural": "natural_qty",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
RESERVED_WORDS = set("""
|
|
68
|
+
all analyse analyze and any array as asc asymmetric authorization binary both case cast
|
|
69
|
+
check collate collation column concurrently constraint create cross default deferrable
|
|
70
|
+
desc distinct do else end except false fetch for foreign freeze from full grant group
|
|
71
|
+
having ilike in initially inner intersect into is isnull join lateral leading left like
|
|
72
|
+
limit natural not notnull null offset on only or order outer overlaps placing primary
|
|
73
|
+
references returning right select similar some symmetric table tablesample then to
|
|
74
|
+
trailing true union unique user using variadic verbose when where window with
|
|
75
|
+
""".split())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def translit(word):
|
|
79
|
+
return "".join(TRANSLIT.get(ch, ch if ch.isalnum() else "") for ch in word.lower())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def words(name):
|
|
83
|
+
"""`мсбРасходЭнергоресурсов` -> ['Расход', 'Энергоресурсов']"""
|
|
84
|
+
return WORD.findall(PREFIX.sub("", name or ""))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_dead(name):
|
|
88
|
+
"""1C keeps retired attributes under an `Удалить...` name. They are not ported."""
|
|
89
|
+
head = words(name)
|
|
90
|
+
return bool(head) and head[0].lower().startswith(terms.DEAD_PREFIXES)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def code(name, max_len=MAX_LEN, avoid_reserved=True):
|
|
94
|
+
"""1C name -> a snake_case dForge code.
|
|
95
|
+
|
|
96
|
+
`avoid_reserved` is on for anything that becomes a Postgres identifier and off for
|
|
97
|
+
anything that becomes a value - a dropdown option code is stored in the column, not
|
|
98
|
+
used as its name, so renaming it would change the data rather than protect it.
|
|
99
|
+
"""
|
|
100
|
+
out = []
|
|
101
|
+
for word in words(name):
|
|
102
|
+
low = word.lower()
|
|
103
|
+
if low in terms.STOP:
|
|
104
|
+
continue
|
|
105
|
+
english = terms.TERMS.get(low)
|
|
106
|
+
if english is None:
|
|
107
|
+
UNKNOWN[low] += 1
|
|
108
|
+
english = translit(low)
|
|
109
|
+
if not english:
|
|
110
|
+
continue
|
|
111
|
+
# `положение` -> `position` twice in a row reads worse than once
|
|
112
|
+
if out and out[-1] == english:
|
|
113
|
+
continue
|
|
114
|
+
out.append(english)
|
|
115
|
+
result = "_".join(out).strip("_")
|
|
116
|
+
result = re.sub(r"_{2,}", "_", result)
|
|
117
|
+
if not result:
|
|
118
|
+
result = translit(name) or "field"
|
|
119
|
+
if result[0].isdigit():
|
|
120
|
+
result = "n" + result
|
|
121
|
+
while len(result) > max_len and "_" in result:
|
|
122
|
+
result = result.rsplit("_", 1)[0]
|
|
123
|
+
result = result[:max_len]
|
|
124
|
+
if avoid_reserved and result in RESERVED_WORDS:
|
|
125
|
+
result = RESERVED_RENAME.get(result, result + "_col")
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def label(name):
|
|
130
|
+
"""1C name or synonym -> an English label. `Количество мест` -> `Qty places`."""
|
|
131
|
+
out = []
|
|
132
|
+
for word in words(name):
|
|
133
|
+
low = word.lower()
|
|
134
|
+
if low in terms.STOP:
|
|
135
|
+
continue
|
|
136
|
+
english = terms.TERMS.get(low)
|
|
137
|
+
if english is None:
|
|
138
|
+
UNKNOWN[low] += 1
|
|
139
|
+
english = translit(low)
|
|
140
|
+
if english and (not out or out[-1] != english):
|
|
141
|
+
out.append(english.replace("_", " "))
|
|
142
|
+
text = " ".join(out).strip()
|
|
143
|
+
if not text:
|
|
144
|
+
text = translit(name)
|
|
145
|
+
return text[0].upper() + text[1:] if text else "Field"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
MISSING_UK = collections.Counter()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def label_uk(name):
|
|
152
|
+
"""1C name or synonym -> a Ukrainian label.
|
|
153
|
+
|
|
154
|
+
Word for word, keeping the order and the case endings the Russian already carries -
|
|
155
|
+
`Расход материалов` is `Витрата матеріалів`. Prepositions are translated rather than
|
|
156
|
+
dropped, because a label is read as a phrase. A word the dictionary does not have is
|
|
157
|
+
left in Russian and counted, so the gap shows up rather than passing as translated.
|
|
158
|
+
"""
|
|
159
|
+
source = [w for w in words(name) if w.lower() not in terms_uk.DROP]
|
|
160
|
+
out, index = [], 0
|
|
161
|
+
while index < len(source):
|
|
162
|
+
phrase = terms_uk.PHRASES.get(
|
|
163
|
+
tuple(w.lower() for w in source[index:index + 2]))
|
|
164
|
+
if phrase is not None:
|
|
165
|
+
out.append(phrase)
|
|
166
|
+
index += 2
|
|
167
|
+
continue
|
|
168
|
+
word = source[index]
|
|
169
|
+
low = word.lower()
|
|
170
|
+
translated = terms_uk.PREPOSITIONS.get(low) or terms_uk.UK.get(low)
|
|
171
|
+
if translated is None:
|
|
172
|
+
MISSING_UK[low] += 1
|
|
173
|
+
translated = word
|
|
174
|
+
if translated and (not out or out[-1] != translated):
|
|
175
|
+
out.append(translated)
|
|
176
|
+
index += 1
|
|
177
|
+
text = " ".join(out).strip()
|
|
178
|
+
if not text:
|
|
179
|
+
return None
|
|
180
|
+
return text[0].upper() + text[1:]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def unique(name, taken, max_len=MAX_LEN, avoid_reserved=True):
|
|
184
|
+
"""`code()` with a numeric suffix if that code is already spoken for in this scope."""
|
|
185
|
+
base = code(name, max_len, avoid_reserved)
|
|
186
|
+
if base not in taken:
|
|
187
|
+
taken.add(base)
|
|
188
|
+
return base
|
|
189
|
+
for n in range(2, 100):
|
|
190
|
+
candidate = "%s_%d" % (base[:max_len - 3], n)
|
|
191
|
+
if candidate not in taken:
|
|
192
|
+
taken.add(candidate)
|
|
193
|
+
return candidate
|
|
194
|
+
raise ValueError("cannot make %r unique" % base)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def report():
|
|
198
|
+
"""How well the dictionary covers the porting scope."""
|
|
199
|
+
import codecs
|
|
200
|
+
import json
|
|
201
|
+
import os
|
|
202
|
+
import attrs
|
|
203
|
+
|
|
204
|
+
root = paths.ROOT
|
|
205
|
+
with codecs.open(os.path.join(root, "out", "report.json"), "r", "utf-8") as f:
|
|
206
|
+
rows = json.load(f)
|
|
207
|
+
objects = attrs.load_json("objects.json")
|
|
208
|
+
types = attrs.load_json("typemap.json")
|
|
209
|
+
|
|
210
|
+
scope = [r for r in rows
|
|
211
|
+
if (r["name"] or "").startswith(binding.client_prefixes())
|
|
212
|
+
and ((r["rows"] or 0) or r["childRows"])]
|
|
213
|
+
|
|
214
|
+
names, fields = 0, 0
|
|
215
|
+
for row in scope:
|
|
216
|
+
code(row["name"])
|
|
217
|
+
label_uk(row.get("synonym") or row["name"])
|
|
218
|
+
names += 1
|
|
219
|
+
_, extracted = attrs.fields_of(row["name"], objects, types)
|
|
220
|
+
for field in (extracted or []):
|
|
221
|
+
if is_dead(field["name"]):
|
|
222
|
+
continue
|
|
223
|
+
code(field["name"])
|
|
224
|
+
label_uk(field.get("synonym") or field["name"])
|
|
225
|
+
fields += 1
|
|
226
|
+
|
|
227
|
+
print("named %d objects and %d fields" % (names, fields))
|
|
228
|
+
|
|
229
|
+
if UNKNOWN:
|
|
230
|
+
print("\nEnglish: %d words fell back to transliteration (%d occurrences)"
|
|
231
|
+
% (len(UNKNOWN), sum(UNKNOWN.values())))
|
|
232
|
+
for word, n in UNKNOWN.most_common(40):
|
|
233
|
+
print(" %-32s %3d -> %s" % (word, n, translit(word)))
|
|
234
|
+
else:
|
|
235
|
+
print("English: every word was in terms.py")
|
|
236
|
+
|
|
237
|
+
if MISSING_UK:
|
|
238
|
+
print("\nUkrainian: %d words left in Russian (%d occurrences)"
|
|
239
|
+
% (len(MISSING_UK), sum(MISSING_UK.values())))
|
|
240
|
+
for word, n in MISSING_UK.most_common(40):
|
|
241
|
+
print(" %-32s %3d" % (word, n))
|
|
242
|
+
else:
|
|
243
|
+
print("Ukrainian: every word was in terms_uk.py")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
if len(sys.argv) > 1:
|
|
248
|
+
for argument in sys.argv[1:]:
|
|
249
|
+
print("%-52s %s" % (argument, code(argument)))
|
|
250
|
+
else:
|
|
251
|
+
report()
|