Compare commits

...
69 Commits
Author SHA1 Message Date
Nick ShirokovandClaude Opus 5 fddaca37e7 docs(meta-edit): убрать из инструкции описание реакции на ошибку
Инструкция навыка отвечает на вопрос «как вызвать правильно». Фразы вида
«опечатка в имени свойства → ошибка (правка не теряется молча)» описывают
поведение при неправильном вызове: модели, которая пишет вызов, они ничего не
дают, а сообщение об ошибке навык и так выдаст сам.

Убраны три таких места в properties-reference.md и child-operations.md.
Соседние утверждения сохранены — это настоящие ограничения: свойство можно
задать, даже если оно ещё не выставлено; допустимы имена свойств
соответствующего типа объекта; структурные свойства через Ключ=Значение не
задаются, кроме Type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:26:47 +03:00
Nick ShirokovandClaude Opus 5 50bc9f5c0a fix(meta-edit): канонизация значений свойств-перечислений
modify-property писал значение свойства объекта в XML как есть: и "обороты", и
"turnovers", и "ЧтоУгодно". Навык печатал Modified: 1 и оставлял выгрузку,
которую платформа не примет. Расхождения портов тут не было — оба вели себя
одинаково, поэтому кампания паритета это не ловила.

Значение теперь проходит через normalize_enum_value — ту же функцию, что уже
применялась к свойствам реквизитов: алиас и регистр приводятся к канону,
неизвестное значение отвергается с перечислением допустимых ДО записи файла.
Поведение сведено к meta-compile, который так работает давно.

Словарь алиасов в meta-edit обёрнут в CIDict: без этого правка сделала бы хуже
— "обороты" строчными не нашли бы ключ "Обороты" и получили бы отказ вместо
прежней тихой записи.

check-inline-drift: заведена семья normalize_enum_value (тела в обоих портах
совпадают побайтово, эталон meta-compile). Списки значений по-прежнему держит
check-enum-drift, новых ключей не заводили.

Проверка: два кейса (синоним → канон в эталоне; мусор → expectError), 685/685
на PS и 682+3 skipped на PY, гарды 4/4. Платформенно: синтетический регистр,
правка через meta-edit, загрузка в 8.3.27 и обратная выгрузка — платформа
вернула Turnovers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:16:07 +03:00
Nick ShirokovandClaude Opus 5 5baece90d6 fix(cf-edit,subsystem-edit,interface-edit): регистр значения операции
Волна закрыла ключи DSL и параметры CLI, но не значения внутри DSL: имя
операции сравнивалось точным совпадением, тогда как PS диспетчеризует через
switch, а он регистронезависим. На cf-edit "Modify-Property" PS применял, а py
писал Unknown operation; на subsystem-edit "Add-Child" py молча ничего не делал.

Имя операции теперь нормализуется в нижний регистр. skd-edit не затронут: у
него операция приходит параметром с choices, что уже закрыто ci_parse_args.

Заодно переписан кейс form-edit/lenient-key-case: он был написан в форме
operations/op, которой у навыка нет, — навык такой вход игнорирует, и кейс
проходил на обоих портах, не проверяя ничего. Теперь форма взята из
документации навыка, а в эталоне есть след эффекта. Кейсы cf-edit,
subsystem-edit и interface-edit расширены регистром значения операции.

Проверка: 683/683 (PS), 680+3 skipped (PY), гарды 4/4, 11 снэпшотов приняты
платформой, версии портов совпадают.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:36:48 +03:00
Nick ShirokovandClaude Opus 5 57b0f95b5f fix(skills): регистронезависимые ключи DSL в py-портах
Дорожка DSL из кампании паритета. PowerShell читает свойства объекта из
ConvertFrom-Json без учёта регистра, Python — точным совпадением, поэтому
"Name" вместо "name" в py-порте молча не находился: навык печатал [OK], а
свойство в выход не попадало. На skd-compile это давало схему без запроса.

CIDict + ci_json (эталон — meta-compile) внесены в 10 портов, читающих
пользовательский DSL: cf-edit, form-compile, form-edit, interface-edit,
meta-edit, mxl-compile, role-compile, skd-compile, subsystem-compile,
subsystem-edit. Обёрнуты точки разбора DSL, операций и JSON, приходящего
значением параметра; реестр .v8-project.json намеренно не трогаем.

skd-edit в список не входит: он принимает операцию через -Operation с
choices, что уже закрыто ci_parse_args.

Каждому навыку добавлен кейс lenient-key-case: вход с перемешанным регистром
ключей, эталон один на оба порта — раннер гоняет обе среды, поэтому кейс сам
по себе проверяет паритет.

Проверка: 683/683 (PS), 680+3 skipped (PY), гарды 4/4, версии портов совпадают.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:15:39 +03:00
Nick ShirokovandClaude Opus 5 d325a3d5af fix(skills): регистронезависимые параметры во всех py-портах
Дорожка CLI из кампании паритета: PowerShell не различает регистр ни в именах
параметров, ни в значениях [ValidateSet], argparse различает и в том и в
другом. Проверено на читающем навыке: cf-info -Mode BRIEF на PS отрабатывает,
на py падал.

ci_parse_args (эталон — meta-compile) вставлен в 68 py-портов, вызовы
parser.parse_args заменены. Навыков с DSL меньше трети, поэтому именно эта
правка делает паритет общим: *-info, *-validate, db-*, cfe-*, web-* тоже
принимают ввод так же, как их .ps1.

Реестр check-inline-drift дополнен списком потребителей — и сразу окупился:
поймал, что массовая замена переписала последнюю строку внутри самого
хелпера и копии стали рекурсивными.

Версии бампнуты синхронно в обоих портах всех 68 навыков.

Проверка: 673/673 (PS), 670+3 skipped (PY), гарды 4/4; паритет версий портов
проверен по заголовкам.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:00:31 +03:00
Nick ShirokovandClaude Opus 5 8c0fbed7d6 fix(meta-compile): регистронезависимый ввод в py-порте — паритет с PS
PowerShell не различает регистр нигде, куда попадает пользовательский ввод:
свойства объекта из ConvertFrom-Json, ключи Hashtable, -eq/-contains, имена
параметров, ValidateSet. Python различает везде, поэтому один и тот же DSL
давал разный результат на разных портах — и чаще всего молча: "CodeLength"
вместо "codeLength" в py просто не находился, навык печатал [OK], а свойство
в выход не попадало.

Пилот на meta-compile. В py-порт добавлены общие обёртки: CIDict (поиск без
учёта регистра, ключи хранятся как есть — часть из них имена объектов и
попадает в XML), ci_json (рекурсивно на разобранный DSL), ci_parse_args (имена
параметров и значения choices). Ими же обёрнуты словари синонимов видов,
алиасов enum и типов.

Попутно исправлен дефект самого канона: PS принимал "type":"catalog", но
дальше использовал значение как есть — в имени тега и в регистрации в
Configuration.xml, то есть отдавал <catalog>, которую платформа не примет.
Теперь вид приводится к канону списка в обоих портах.

check-inline-drift: extractPy научен доставать class (иначе тело CIDict
невидимо), заведены три семьи с ps1: null — в PS1 этих обёрток быть не должно.

Проверка: кейс lenient-key-case зелёный на обоих портах; полный регресс
673/673 (PS) и 670+3 skipped (PY); гарды 4/4; sweep по 400 справочникам трёх
конфигураций (декомпиляция → компиляция обоими портами) — 0 расхождений.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 19:48:24 +03:00
Nick ShirokovandClaude Opus 5 4622234b52 feat(meta-compile): пары субконто бухрегистра из плана счетов
Компилятор эмитил ExtDimensionN/ExtDimensionTypeN только по ключам DSL,
поэтому регистр, созданный по неполному описанию, не совпадал с тем, что
материализует платформа: пары дописывались лишь при загрузке.

Теперь их число выводится из MaxExtDimensionCount плана счетов, на который
ссылается регистр, — файл читается из выгрузки, как версия формата из
Configuration.xml. ExtDimensionN получает LinkByType на Account с LinkItem
по номеру. Выведенный хвост дополняет DSL, а не заменяет: ключи из описания
остаются, недостающее добавляется.

План счетов не найден в выгрузке (ещё не создан, лежит вне каталога) — пары
не генерируются, в вывод идёт [HINT]: платформа допишет их сама при загрузке.

Проверка: матрица из трёх случаев (план с 3 субконто, план с 0, план
отсутствует) на обоих портах; синтетика загружена в 1С и выгружена обратно —
состав и порядок совпали; корпусный роундтрип 739 регистров без расхождений.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:42:00 +03:00
Nick ShirokovandClaude Opus 5 16199deb92 fix(meta-compile,meta-validate): канон стандартных реквизитов регистров
Справочник порядка StandardAttributes был неполон у трёх видов регистров, а
неизвестные имена эмитятся перед фикс-списком — платформа их вкрапляет, и
роундтрип переставал быть побайтовым.

Порядок снят с выгрузки платформы, присутствие условных реквизитов теперь
выводится из свойств объекта (periodAdjustmentLength, correspondence,
registerType), а не из наличия ключа в DSL: иначе регистр, создаваемый с нуля,
терял реквизит, который платформа обязана материализовать. Наличие ключа
осталось дополняющим условием, чтобы роундтрип не зависел от точности вывода.

- регистр расчёта: 11 реквизитов, состав безусловен (проверено матрицей
  actionPeriod × basePeriod × периодичность на 8.3.27)
- регистр бухгалтерии: PeriodAdjustment при длине периода корректировки > 0,
  RecordType при correspondence=false
- регистр накопления: RecordType только у регистра остатков

meta-validate: снято ложное предупреждение об «unexpected» RecordType у
бухрегистра, условные реквизиты исключены из проверки missing.

Проверка: 739 регистров по шести конфигурациям — 0 расхождений; снэпшоты
пересняты и верифицированы загрузкой в 1С.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:06:34 +03:00
Nick ShirokovandClaude Opus 5 707a5c293f fix(cfe-patch-method): паритет портов по прощающему вводу + карта в реестр
Проверка «работает ли гард во все стороны» вскрыла, что он проверяет только
заявленные карты. Попытка автообнаружения незаявленных дала 112 срабатываний,
из них большинство — ложные (накопители массивов, чей блок разбирается неверно),
поэтому сама проверка в гард не попала. Но она нашла реальное:

у cfe-patch-method карта типов не была в реестре, и порты по ней разошлись —
PS1 принимал и Catalog.X, и Catalogs.X (32 записи), PY только Catalog.X (16).
Расхождение доставшееся, не из этой ветки. PY дополнен формами множественного
числа: ввод, работавший в одном порте, теперь работает в обоих.

В реестр карт типов добавлены cfe-patch-method (TYPE_DIR_MAP, DIR_TO_TYPE),
cf-edit.RU_TYPE_MAP и cfe-borrow.SYNONYM_MAP — 28 проверяемых карт стало 36.
Для частичных карт добавлен флаг partial (полнота не требуется, каталоги
сверяются), для прощающих — keyMayBeDir (ключом принимается имя каталога).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 15:27:44 +03:00
Nick ShirokovandClaude Opus 5 04d78fe816 chore(*-info): бамп версий после переименования хелпера
Шесть *-info изменены переименованием is_external_root ->
_sg_is_external_root без бампа версии. Поднято в обоих портах синхронно, как
требует конвенция, хотя менялся только .py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:57:34 +03:00
Nick ShirokovandClaude Opus 5 63e711905c fix(tests): экстракторы гарда пропускали копии — ложное «OK»
Разбор остатка по support-guard показал, что расхождения портов у *-info нет:
они читают тем же хелпером состояние поддержки для вывода, и фича есть в обоих
портах. Пропускал копии сам гард.

Два дефекта извлечения, оба давали ложное «OK», а не ложную тревогу:

1. PY: вложенные определения. Пять *-info объявляют is_external_root внутри
   другой функции; экстрактор искал только `^def` и перескакивал через тело
   внешней функции, так что вложенные для гарда не существовали.
2. PS1: однострочные функции. subsystem-info.ps1:18 — `function Out(...) { ... }`
   в одну строку; поиск закрывающей `}` на отдельной строке делал «телом» Out всё
   до следующей одиночной скобки, проглатывая следующую функцию.

После починки гард увидел 9 копий, которых не видел, — все совпали с эталонами.
Побочно вскрылось, что Report-OK в interface/subsystem/xdto-validate тоже был
невидим и потому не попал в прошлое сведение Report-*; теперь сведён.

Осталось расхождение имён: одно тело называлось _sg_is_external_root (17),
is_external_root (5 *-info) и _meta_is_external_root (meta-info). В PS1 имя было
единым изначально; PY сведён к _sg_is_external_root.

Реестр: 24 семьи, 319 копий (было 310), долг ноль.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:42:46 +03:00
Nick ShirokovandClaude Opus 5 78ac3a8c36 fix(xdto-compile,xdto-edit): заменить урезанный support-guard на общую реализацию
Своя версия решала по правилу «файл Ext/ParentConfigurations.bin существует →
запретить». Общая разбирает заголовок bin и решает по конкретному объекту; в
частности у неё есть ветка `if k == 0: return` — если на поддержке нет ни одного
объекта, запрещать нечего.

Эксперимент на копии каталога конфигурации ERP (заголовок bin = {6,0,0,0,1,0},
K=0): meta-compile создавал объект, xdto-compile на том же каталоге отказывал.
То есть XDTO-пакет нельзя было добавить туда, где справочник добавляется
свободно. У acc_8.3.24 и ut_8.3.27 заголовок {6,1,1,...} (G=1, вся конфигурация
read-only) — там обе реализации отказывали одинаково.

Своя версия строго более ограничительна, поэтому это не дыра, а ложные отказы
плюс неинформативное сообщение. После правки ERP разрешает, acc по-прежнему
отказывает — и уже с диагнозом и шагами через support-edit.

Долг реестра inline-реализаций обнулился: 24 семьи, 310 копий, вариантов без
обоснования не осталось.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:23:37 +03:00
Nick ShirokovandClaude Opus 5 2eb25be7c5 test(skills): гард согласованности карт типов метаданных со спецификацией
Прямой ответ на вопрос ишью #60. Гард inline-реализаций держал функции, но не
словари — а именно там расхождение и накапливалось молча: тип Bot существовал в
таблице спецификации и в трёх навыках, в остальных двенадцати его не было, и
заметить это было нечем.

Эталон — таблица «Порядок типов в ChildObjects» из docs/1c-configuration-spec.md
(45 типов: имя, каталог, позиция). Берём документацию, а не отдельный JSON: тогда
спека и код не расходятся молча, что и было целью ишью.

Модель двухуровневая, как и предлагалось в обсуждении: общее ядро (имя, каталог,
порядок) обязано совпадать у всех, а навык объявляет своё подмножество —
исключение с ПРИЧИНОЙ. Проверка отличает намеренное ограничение от забытого типа.
Сейчас исключение ровно одно: Language в cfe-diff, где записи в карте были бы
недостижимы.

Вокабуляры навыков (TYPE_ALIASES, TYPE_NORM_MAP, CONTENT_TYPE_MAP, TYPE_PLURAL_MAP)
проверяются слабее: полнота не требуется, но каждое каноническое имя обязано
существовать в таблице — это ловит опечатки. Пустое извлечение карты считается
ошибкой разбора, иначе непонятый формат прошёл бы вхолостую.

Проверено негативом: убранный тип, подменённый каталог и переставленный порядок
дают ERROR и exit 1.

cfe-borrow/SKILL.md: убрано обещание про количество поддерживаемых типов — оно уже
протухло (было 44) и является обязательством, которое навык не проверяет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:57:50 +03:00
Nick ShirokovandClaude Opus 5 34ac9a18d0 fix(interface-edit,role-compile): дополнить русские имена типов
interface-edit знал CalculationRegisters и ChartsOfCalculationTypes только
по-английски: РегистрРасчёта/РегистрРасчета и ПланВидовРасчёта/ПланВидовРасчета
не принимались ни в каком написании. Добавлены также Роль, ОбщийМакет,
ЭлементСтиля, ОбщийРеквизит, ГруппаКоманд — в единственном и множественном числе.

role-compile принимал только формы без ё (Отчет, РегистрРасчета,
ПланВидовРасчета), тогда как meta-compile, form-compile и subsystem-навыки
принимают обе. Добавлены варианты с ё.

Правка аддитивная: ввод, который работал, продолжает работать.

cfe-borrow/SKILL.md: «все 44 типа» -> 45 (таблица ChildObjects включает Bot и
Language, оба подтверждены платформенной пробой).

Кейс role-compile/type-aliases-yo проверен негативным прогоном на своём порте.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:45:23 +03:00
Nick ShirokovandClaude Opus 5 a0a91a4e40 fix(cfe-diff,cfe-borrow,cfe-validate,subsystem-*): вернуть недостающие типы
Платформенная проба (свежий стенд 8.3.24, загрузка + обратная выгрузка) сняла
вопросы, которые ревизия #60 оставила на догадках:

  Bot в ChildObjects конфигурации      — принят, пережил раундтрип
  Bot.X в <Content> подсистемы          — принят, пережил раундтрип
  Bot в ChildObjects расширения         — принят, пережил раундтрип
  второй Language заимствован в расш.   — принят, пережил раундтрип

Порядок, выданный платформой (Language, Subsystem, Bot), совпал с таблицей
docs/1c-configuration-spec.md, где Bot стоит под № 11.

Правки:
- cfe-diff: +Style, +XDTOPackage, +WebService, +HTTPService, +WSReference, +Bot
  (38 -> 44). Карта используется в трёх местах — обзор Mode A, поиск модулей и
  проверка переноса Mode B, — поэтому объект неизвестного типа выпадал из всех.
- cfe-borrow: +Bot, +Language (43 -> 45), Bot в TYPE_ORDER;
- cfe-validate: +Bot (44 -> 45);
- subsystem-compile, subsystem-edit: +Bot в CONTENT_TYPE_MAP.

Language в cfe-diff НЕ добавлен: навык намеренно пропускает языки при сборе
объектов (cfe-diff.py:530), запись в карте была бы недостижима.

Прежняя оценка «отсутствие Language в cfe-borrow вероятно законно» опровергнута:
в расширении из корпуса язык помечен ObjectBelonging=Adopted, то есть заимствован,
и второй язык заимствуется штатно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:31:11 +03:00
Nick ShirokovandClaude Opus 5 ff01102b9b docs(tests): зафиксировать природу расхождения оставшихся семей
Храповик без пояснения читается как «когда-нибудь свести». Разбор показал, что
из четырёх оставшихся семей одна вообще не является общей: import_fragment имеет
разные сигнатуры и разные наборы xmlns по навыкам — одно имя на разные задачи,
сводить его нельзя. Остальные три сводятся, но каждое сведение меняет вывод и
требует решения, а не аккуратности.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:30:54 +03:00
Nick ShirokovandClaude Opus 5 01749ad6ee refactor(skills): свести write_xml_file и write_utf8_bom к общему эталону
У write_xml_file в docstring прямо написано «Держать копии одинаковыми —
сознательно: разошедшиеся копии сводят на нет весь смысл», и при этом копий было
три варианта. Различие оказалось не в логике нормализации, а в имени
вспомогательной функции записи байт: write_utf8_bom (11 навыков),
write_text_with_bom (form-add, help-add, template-add), save_text_bom
(cfe-borrow) — тела всех трёх побайтово эквивалентны.

Имя сведено к write_utf8_bom, тело помощника — к общему эталону (различались
только имена переменных). PS1-порт Write-XmlFile расхождений не имел.

Обе семьи переведены из храповика в реестр с эталоном: 24 семьи, 309 копий,
разъехавшихся осталось 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:29:43 +03:00
Nick ShirokovandClaude Opus 5 f2216745e6 refactor(validate): свести Report-* к общему эталону вывода
interface-validate, subsystem-validate и xdto-validate отличались от остальных
шести валидаторов только стилем: инлайн-сигнатура вместо param-блока и
однострочный if. Приведены к эталону cf-validate.

form-validate и mxl-validate оставлены отдельным вариантом с обоснованием:
они пишут через Write-Host, а не через буферизованный Out-Line. Буферизация
нужна для -OutFile (тот же отчёт в файл), которого у этих двух навыков нет;
для модели-потребителя stdout одинаков — на успешном прогоне обе ветки дают
одну строку.

Семьи Report-OK/Error/Warn переведены из храповика в реестр с эталоном.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 21:23:58 +03:00
Nick ShirokovandClaude Opus 5 fadda6a287 refactor(meta-validate,web-publish): свести format_rank и db-обвязку к эталонам
format_rank в meta-validate отличался только именем параметра, _version_key в
web-publish инлайнил каталог версии вместо общего _version_dir (и знал только
Windows-раскладку .../bin/exe). Обе копии приведены к эталону db-create/
meta-compile; для Windows-навыка web-publish поведение не меняется.

В реестр добавлены семьи _version_dir и _find_project_v8path.

Экстрактор PS1 в гарде и сканере научен видеть вложенные определения: db-load-git
объявляет Find-ProjectV8Path внутри if, и поиск по `function` с нулевой позиции
считал функцию отсутствующей. Конец тела теперь ищется по закрывающей скобке на
отступе самого слова function.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 21:13:50 +03:00
Nick ShirokovandClaude Opus 5 974ca3aac9 fix(form,skd,init,meta-edit): развести экранирование атрибута и текста
esc_xml был один на две задачи, и в атрибутном контексте применялся текстовый
вариант без &quot;. form-compile с choiceParameters, имя которого содержит
кавычку, выдавал <app:item name="Отбор.Наименование "кавычка"">: lxml падает с
attributes construct error, то есть файл невалиден как XML. Затронуто 9 мест в
form-compile и 3 в skd-compile, одинаково в обоих портах.

Раундтрип через базу показал границу: в ТЕКСТЕ элемента платформа экранирует
только & < > (кавычка и апостроф возвращаются сырыми байт-в-байт), а в ЗНАЧЕНИИ
АТРИБУТА пишет &quot; — внутри "..." литеральная кавычка невалидна. Поэтому две
функции с говорящими именами, а не одна с флагом:
  esc_xml      — значение атрибута: & < > "
  esc_xml_text — текст элемента:    & < >

Заодно закрыто расхождение портов в init-навыках: PY экранировал текст с &quot;,
PS1 — через SecurityElement::Escape (ещё и &apos;), а epf-init/erf-init в PS1 не
экранировали вовсе, из-за чего амперсанд в синониме давал невалидный XML.

Кейсы: form-compile/attr-value-escaping, skd-compile/additional-properties-escaping,
cf-init/synonym-escaping — все три проверены негативным прогоном на обоих портах
и верификацией снэпшотов на платформе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:53:12 +03:00
Nick ShirokovandClaude Opus 5 3c50d3caa0 fix(form,meta,skd): срезать любой сгенерированный префикс dNpM:, не только d5p1
Имя префикса для .../8.1/data/enterprise/current-config платформа выдаёт по
порядку объявления в файле: в корпусе acc_8.3.27 на этом URI встречаются d4p1,
d5p1 и d6p1. Литеральное d5p1: покрывало лишь часть случаев — тип, скопированный
из Predefined.xml, приходит с d4p1: и даёт ту же ошибку «Неизвестное имя типа».

Обобщено до ^d\d+p\d+: — так же, как это уже делает form-info. Условие «только у
ссылочных типов, с точкой» сохранено: все 11 спец-типов чужих пространств имён
(d5p1:Chart, mxl:SpreadsheetDocument, pl:Planner и др.) точки не имеют.

Регресс-кейс расширен на d4p1 и d6p1: все шесть форм ввода дают один
cfg:CatalogRef.Склады.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:04:06 +03:00
Nick ShirokovandClaude Opus 5 826ba3de71 fix(form,meta,skd): resolve_type_str — одно тело + срезание префикса типа
Шесть копий resolve_type_str разошлись на 6 вариантов в PY и 4 в PS1, а вместе
с формой разошлось и поведение: префикс ссылочного типа срезал только
form-compile.

Проверка на платформе (стенд 8.3.24.1691, cf-init → meta-compile → db-load-xml)
показала, что это не косметика. Префикс ломает поиск в словаре синонимов, из-за
чего русское имя типа остаётся непереведённым:
  СправочникСсылка.Склады      → cfg:CatalogRef.Склады        (верно)
  cfg:СправочникСсылка.Склады  → cfg:СправочникСсылка.Склады  (неверно)
Платформа отвечает «Неизвестное имя типа - СправочникСсылка.Склады» и
отклоняет загрузку. Именно с префиксом тип и написан в выгрузке, откуда
пользователь его копирует.

cfg: снимается всегда, d5p1: — только у ссылочных типов (с точкой): сам по себе
префикс неоднозначен, в формах d5p1:Chart, d5p1:TextDocument и ещё три типа
адресуют свои пространства имён, и там он часть канонического значения. Первая
версия правки срезала его безусловно и уронила 4 кейса form-compile.

Тело общее для всех шести навыков; словари синонимов остаются локальными, навык
объявляет на свой словарь алиас TYPE_SYNONYMS / $script:typeSynonyms.

Регресс-кейс meta-compile/type-prefix-lenient проверяет все три формы ввода.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 19:44:39 +03:00
Nick ShirokovandClaude Opus 5 d99d63288e refactor(skills): detect_format_version — один вариант на всех потребителей
Семья была разбита на «знает про автономную EPF/ERF» (form-*, mxl-compile,
template-add, help-add) и «не знает» (cfe-borrow, interface-edit, meta-compile,
role-compile, subsystem-compile, xdto-compile), плюс редакторски разошедшаяся
PS1-копия help-add.

Расширенный вариант корректен и для конфигурационных навыков: ветка срабатывает
только если <каталог>.xml — корень ExternalDataProcessor/ExternalReport, чего в
дереве конфигурации не бывает. Переключатель не нужен — раскопирован как есть.

Семья в реестре схлопнута до одного варианта: 11 копий, оба порта.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 18:18:52 +03:00
Nick ShirokovandClaude Opus 5 1586e909a9 fix(meta-validate,meta-edit): ReuseSessions — вернуть значение Use
Оба навыка бракуют корректное значение: allowlist содержал только
[DontUse, AutoUse], тогда как meta-compile знает три значения.

Прав meta-compile: docs/meta-dsl-spec.md перечисляет DontUse / Use / AutoUse,
в выгрузке прикладной конфигурации встречаются все три (DontUse ×17,
AutoUse ×2, Use ×1).

Расхождение ловил check-enum-drift.mjs, но гард не был подключён ни к runner-у,
ни к README, поэтому не запускался. Теперь он входит в check-all.mjs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 18:07:46 +03:00
Nick ShirokovandClaude Opus 5 c8144a01e5 test(skills): реестр и гард общих inline-реализаций
Навыки автономны, общие утилиты копируются в каждый .ps1/.py, но нигде не было
зафиксировано, какая копия эталонная и какие расхождения законны. Ревизия по #60
показала два класса семей: одни держатся побайтово (support-guard — 16 копий,
db-обвязка — 12), другие разъехались без эталона (resolve_type_str — 6 копий и
6 вариантов, get_ml_text — 7 и 7).

check-inline-drift.mjs держит реестр семей внутри себя: вариант → эталон → копии.
Копия обязана совпадать с эталоном своего варианта; отклоняющийся вариант обязан
иметь обоснование, иначе печатается как долг. Часть расхождений законна (esc_xml
без &quot; в form-* ради раундтрипа), поэтому модель хранит варианты, а не одно
эталонное тело. Разъехавшиеся целиком семьи стоят на храповике maxVariants.

Извлечение тел: PS1 — до строки ровно `}` (балансировка скобок даёт ложные
18 вариантов из 18 копий Assert-EditAllowed); PY — с обязательным снятием
docstring-ов (иначе одинаковый код с разным описанием читается как расхождение).

check-all.mjs — единая точка входа: check-enum-drift и check-uuid-invariant были
рабочими, но не упоминались в README и никем не запускались.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 17:22:33 +03:00
Nick ShirokovandClaude Opus 5 fc2d460b57 docs(web-info,web-publish): убрать детали реализации из инструкций
В «Как читать вывод» протекли внутренности фильтра журнала (коды сообщений
Apache, что именно отсеивается и почему) и обоснование правки. Модели,
использующей навык, нужны сигналы и действие по ним, а не устройство.
В web-publish по той же причине убрано имя атрибута дескриптора.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:48:32 +03:00
Nick ShirokovandClaude Opus 5 0d4933f135 feat(web-info): проверка TCP-порта Apache
Статус строился только по наличию процесса httpd: при живом процессе и
не поднявшемся сервере (занятый порт, битый конфиг) навык рапортовал
«Запущен». Теперь порт из Listen проверяется TCP-коннектом на 127.0.0.1
с таймаутом 1 с — «(слушается)» / «(не отвечает)».

Сами публикации намеренно не опрашиваются: запрос к /{AppName} поднимает
сеанс веб-клиента 1С и занимает лицензию, а публикаций может быть много.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:46:22 +03:00
Nick ShirokovandClaude Opus 5 b8cbb0111a feat(web-info): в секции ошибок только error и выше + путь к журналу
Печатались последние 5 строк журнала как есть, поэтому секция «Последние
ошибки» показывала notice старта, а на Windows ещё и штатный шум
winnt_accept — содержательные записи вытеснялись. Теперь фильтр по уровню
error/crit/alert/emerg; AH02538 отсеивается отдельно как след нашего же
рестарта Apache при публикации.

Путь к журналу печатается всегда — за отсеянными уровнями и полным
контекстом читается файл.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:37:24 +03:00
Nick ShirokovandClaude Opus 5 88535a759b fix(web-info): имя журнала ошибок из httpd.conf + описание вывода вместо примера
Секция «Последние ошибки» всегда печатала «(нет файла)»: путь был захардкожен
как logs/error.log, а сборка Apache пишет logs/error_log. Имя берём из директивы
ErrorLog, с фолбэком на оба общепринятых имени.

В SKILL.md дословный пример вывода заменён легендой: модель и так видит реальный
stdout, ей нужна семантика тегов и признаков, а не их копия (и копия дрейфует).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:28:20 +03:00
Nick ShirokovandClaude Opus 5 fbf4d4f986 fix(web-info): детект OData по <standardOdata enable> + метка сервисов расширений
Тег [OData] искался по атрибуту enableStandardOdata на <point> — форма до
8.3.9, которую web-publish не пишет с перехода на <standardOdata enable="true"/>,
поэтому в сводке не появлялся никогда. Старая форма оставлена как fallback
для дескрипторов не нашего происхождения.

Плюс суффикс +ext на WS/HTTP, когда в дескрипторе publishExtensionsByDefault.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:20:20 +03:00
Nick ShirokovandClaude Opus 5 1eeea204c3 feat(web-publish): публикация сервисов расширений в default.vrd
publishExtensionsByDefault="true" на <ws> и <httpServices>: сервисы,
поставляемые расширениями конфигурации, публикуются вместе с сервисами
самой конфигурации. Без атрибута HTTP-сервис расширения отдаёт 404,
web-сервис — 500 «Сервис не найден».

Проверено E2E на 8.3.24 (A/B на одном дескрипторе, оба порта).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 15:19:08 +03:00
Nick ShirokovandClaude Opus 5 27e78859e8 fix(tests): отпечаток фикстуры рядом с каталогом, а не внутри
Файл .fixture-stamp лежал внутри каталога фикстуры, а фикстура копируется в
рабочий каталог кейса целиком — и отпечаток попал в 11 снэпшотов. Перенесён в
<cache>/<setup>.stamp, из снэпшотов удалён.

663/663 ps1, 660/663 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 13:53:25 +03:00
Nick ShirokovandClaude Opus 5 aa32fb84dc test: снята мёртвая нормализация регистра encoding в снэпшотах
Третья маска подряд, на которую не опирался ни один кейс: py-набор без неё
зелёный целиком (660/663). Порты пишут XML-декларацию одинаково.

Осталась одна нормализация — схлопывание пробелов между тегами. Она живая:
без неё падают 58 кейсов в 15 навыках, то есть порты реально отступают
по-разному. Это отдельная задача, комментарий в раннере теперь называет цифру,
чтобы маска не выглядела безобидной.

663/663 ps1, 660/663 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 13:40:53 +03:00
Nick ShirokovandClaude Opus 5 4c84d840df fix(tests): инвалидация кэша фикстур empty-config по содержимому cf-init
Каталог фикстуры кэшировался навсегда: правка cf-init до него не доходила,
снэпшоты записывались со старой фикстурой, и расхождение всплывало только на
машине с пустым кэшем. Поймано прогоном на маке — там фикстура собралась заново
и принесла TextToSpeech, которого нет в снэпшотах, записанных на Windows.

Теперь рядом с фикстурой лежит отпечаток: аргументы плюс хэши обоих портов
cf-init. Не совпал — каталог пересобирается.

Снэпшоты кейсов 2.21 перезаписаны со свежей фикстурой (дрейф — только
TextToSpeech). У mxl-compile снэпшот кейса снят через noSnapshot: у навыка
normalizeUuids=false, поэтому UUID фикстуры дрожали бы при каждой пересборке,
а предмет кейса проверяет fileContains.

663/663 ps1, 660/663 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 12:53:38 +03:00
Nick ShirokovandClaude Opus 5 57c8cb6d23 feat(cfe-init): Caption и ShortCaption в корне расширения на формате 2.21
Свойства корня, добавленные форматом 2.21, между Version и DefaultLanguage.
В cf-init они уже были, в cfe-init — нет. Нашёл раундтрип расширения через базу
8.5 по сценарию из issue #57: загрузка расширения и обратная выгрузка давали
диff ровно на этих двух элементах.

После правки выгрузка расширения из базы 8.5 совпадает с исходниками полностью —
и по составу файлов, и побайтово.

663/663 ps1, 660/663 py; 1С-сертификация cfe-init и cfe-borrow на 8.5 — 2/2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 23:08:18 +03:00
Nick ShirokovandClaude Opus 5 3b1a2f64d5 feat(cf-init,form-add): TextToSpeech и UseInInterfaceCompatibilityMode в формате 2.21
Оба пробела нашёл цикл через базу 8.5 (собрать навыками → загрузить → выгрузить):

- cf-init не писал возможность мобильного приложения TextToSpeech, добавленную
  форматом 2.21 последней в списке UsedMobileApplicationFunctionalities;
- form-add не писал <UseInInterfaceCompatibilityMode>Any</…> в метаданных формы.
  meta-compile эмитил это свойство только для общей формы, а платформа 8.5 пишет
  его у форм любых объектов — сразу после UsePurposes, до ExtendedPresentation.

После правки выгрузка платформы совпала с исходниками везде, кроме
ConfigurationExtensionCompatibilityMode (платформа поднимает значение до своего
максимума — так и задумано), скелета Ext/Form.xml и тела MXL: оба расхождения
предсуществующие, к формату 2.21 не относятся.

663/663 ps1, 660/663 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 22:25:50 +03:00
Nick ShirokovandClaude Opus 5 6315a706b5 test: кейсы формата 2.21 для навыков-эмиттеров
Фикстура empty-config-221 (cf-init -FormatVersion 2.21) и по кейсу на навык:
cf-init, meta-compile, meta-edit, form-add, form-compile, template-add,
mxl-compile, subsystem-compile, subsystem-edit, role-compile, xdto-compile,
cfe-init, cfe-borrow, epf-init, erf-init.

Проверки через expect.fileContains по сырым байтам, а не только снэпшотом:
позиция xmlns:pal (после lf, перед style) и его отсутствие там, где платформа
его не пишет (Rights.xml роли, Ext/ClientApplicationInterface.xml).

663/663 ps1, 660/663 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:56:26 +03:00
Nick ShirokovandClaude Opus 5 3ef9e76f03 feat(epf-init,erf-init,form-add,form-compile,template-add,mxl-compile,help-add): версия формата у автономных EPF/ERF
Детекторы версии искали только Configuration.xml, поэтому внутри автономной
внешней обработки или отчёта всегда получалось 2.17 — независимо от version в
корне самого объекта. Теперь версия наследуется от корня EPF/ERF: подъём по
дереву проверяет и <каталог>.xml с корнем ExternalDataProcessor/ExternalReport,
а form-add и template-add читают её прямо из файла объекта, с которым работают.

Только после этого у epf-init/erf-init появился параметр -FormatVersion
(2.17…2.21, дефолт 2.17 — прежнее поведение). Без наследования он дал бы
обработку 2.21, внутри которой формы и макеты остаются 2.17.

Проверено сквозным прогоном: epf-init -FormatVersion 2.21 → form-add →
template-add → mxl-compile, все файлы 2.21 с xmlns:pal, порты идентичны.

Снэпшоты role-info/role-validate/cf-edit обновлены — их фикстуры строит
role-compile, дрейф только от смены его формата на платформенный.

648/648 ps1, 645/648 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:41:42 +03:00
Nick ShirokovandClaude Opus 5 5bcf6d08cd fix(role-compile): формат Role.xml и Rights.xml как у платформы
Навык писал шапку многострочно (каждое объявление своей строкой) и отступы
четырьмя пробелами, тогда как платформа пишет шапку одной строкой и отступы
табами. На каждой роли это давало диff во всю длину файла — шум, из-за которого
реальные расхождения не видны.

Сверено с выгрузкой УНФ 8.5: скелет Role.xml совпал с платформенным полностью
(с точностью до uuid и текстов). 1С-сертификация: 10/10 кейсов загружаются.
Снэпшоты обновлены — дрейф только ожидаемый (склейка шапки и пробелы→табы).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:13:35 +03:00
Nick ShirokovandClaude Opus 5 5c3449d6c2 feat(mxl-compile): определение версии формата + xmlns:pal в 2.21
У корня <document> нет атрибута version, поэтому версия берётся из конфигурации,
в дерево которой пишется макет (подъём до Configuration.xml, как в остальных
навыках); вне конфигурации остаётся прежнее поведение — 2.17.

Платформа 8.5 объявляет палитру и в теле MXL: в выгрузке УНФ 8.5 xmlns:pal стоит
у всех 3471 файлов с корнем document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:01:45 +03:00
Nick ShirokovandClaude Opus 5 53c1d59ec1 feat(cfe-borrow,cfe-init,form-add,form-compile,role-compile,subsystem-compile,subsystem-edit,template-add,xdto-compile): xmlns:pal в формате 2.21
Платформа 8.5 объявляет пространство палитры в шапках MetaDataObject и Form.
Радиус снят по выгрузке УНФ 8.5, а не угадан: pal есть у MetaDataObject, Form,
document (тело MXL), Style, AppearanceTemplate, GraphicalSchema; у Rights, Help,
CommandInterface, DataCompositionSchema и корней extrnprops его нет. Поэтому
role-compile правит шапку Role.xml и не трогает Rights.xml.

Вставка идёт на место — после lf, перед style: объявления платформа держит по
алфавиту, дописать в конец нельзя.

Попутно в cfe-borrow тег <Form> больше не копируется из исходной формы целиком:
из него берутся только объявления пространств, а version подставляется своя.
Раньше версия источника молча побеждала — вопреки комментарию рядом.

Тесты: снята нормализация xmlns в снэпшотах py-прогона (она прятала ровно этот
класс расхождений и к моменту снятия была мёртвой — ни один кейс на неё не
опирался); добавлены expect.fileContains/fileNotContains по сырым байтам и гейт
на неизвестные ключи expect.* — он вскрыл два кейса-пустышки (skd-compile,
skd-edit), они починены.

648/648 ps1, 645/648 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 20:57:55 +03:00
Nick ShirokovandClaude Opus 5 d60dd47f0b refactor(cfe-init,form-add,form-compile,subsystem-compile,subsystem-edit,template-add,xdto-compile): шапка XML одной переменной
Объявления пространств имён вынесены в переменную рядом с определением версии
формата; места эмиссии её только интерполируют. Раньше шапка была вклеена в
литерал в каждой точке записи, и любая правка шапки ложилась в каждый навык
по-своему.

Попутно в form-compile убрана мёртвая эмиссия Title и шапки перед пересозданием
буфера (её результат затирался).

Вывод байт-в-байт: 648/648 ps1, 645/648 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 20:33:42 +03:00
Nick ShirokovandClaude Opus 5 34b533a730 feat(meta-compile,meta-edit,cf-init,meta-validate): формат выгрузки 2.21 (8.5)
Дельта 2.20 → 2.21 снята синтетическим экспериментом: одна и та же минимальная
конфигурация загружена в две пустые базы, 8.3.27.1859 и 8.5.1.1302, и выгружена
из каждой (стенд debug/fmt221). Всё, что нашлось раундтрипом по УНФ,
воспроизвелось на синтетике — значит это ступень формата, а не особенность
конфигурации.

Версию формата задаёт ПЛАТФОРМА ВЫГРУЗКИ, а не режим совместимости:
CompatibilityMode в обеих выгрузках остался Version8_3_24, но 8.5 всё равно
написала 2.21. Поэтому нигде не появляется привязки к режиму совместимости —
только к версии из Configuration.xml, как и было устроено.

Реализовано:
- xmlns:pal (палитра) в шапках MetaDataObject и Form у meta-compile и cf-init.
  Вставка НА МЕСТО (после lf, перед style) — платформа держит объявления по
  алфавиту. В файлы с корнем extrnprops (Ext/ClientApplicationInterface.xml)
  платформа его не пишет, и мы не пишем.
- meta-compile: <Color>auto</Color> у значения перечисления, <AuxiliaryVariantForm/>
  у отчёта, <UseInInterfaceCompatibilityMode>Any</…> у общей формы. Позиции взяты
  из эталонной выгрузки, а не угаданы.
- meta-edit: <Color> при добавлении значения перечисления — навык сам эмитит
  EnumValue, и без этого добавленное значение отличалось бы от соседних.
- cf-init: 14 новых свойств корня при -FormatVersion 2.21.
- meta-validate: лестница версий (был единственным валидатором без 2.21) и три
  строки в реестре versionedProps — механизм для этого и заводился.

Проверка:
- наш вывод на 2.21 совпал с выгрузкой платформы ПОСТРОЧНО по Enums, Reports и
  CommonForms; порты идентичны modulo UUID;
- на 2.17-2.20 вывод не изменился ни в одном байте;
- юнит-тесты 648/648 ps1, 645/648 py.

Известное отклонение, НЕ относящееся к 2.21: скелет Ext/Form.xml общей формы
отличается от платформенного (dcssch в шапке, форма AutoCommandBar, Attributes) —
то же расхождение есть и на 2.20, разбирать отдельно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 19:39:31 +03:00
Nick ShirokovandClaude Opus 5 7892c9fbfa fix(meta-compile): позиция условных стандартных реквизитов бухрегистра
У бухрегистра условные стандартные реквизиты стоят по обе стороны фикс-списка:
PeriodAdjustment — перед Account, пары субконто — после Period. Эмиттер же клал
все ключи, которых нет в списке типа, скопом вперёд, поэтому порядок расходился
с выгрузкой.

Разделено на три части: ключи вне списка по умолчанию идут ПЕРЕД ним (легаси
вроде ExchangeDate у части планов обмена), подходящие под хвостовой шаблон типа —
ПОСЛЕ, а именованные условные (PeriodAdjustment) занимают своё место в самом
списке и эмитятся при наличии в DSL.

Пары субконто заданы ШАБЛОНОМ, а не списком имён: их количество определяется
свойством MaxExtDimensionCount плана счетов. В корпусе везде 3, но это
однородность выборки, а не правило, и список из ExtDimension1..3 закрыл бы
только наблюдаемый случай. Хвост сортируется по номеру, внутри номера
ExtDimensionN идёт перед ExtDimensionTypeN.

Проверка:
- синтетика с ПЯТЬЮ парами субконто и перемешанными ключами на входе даёт
  канонический порядок с пропусками номеров; порты байт-в-байт (modulo UUID);
- раундтрип «все виды» (112 объектов, 42 вида): порядок ≠ 0, TOTAL diff 0;
- юнит-тесты 648/648 ps1, 645/648 py; 1С-сертификация accounting-register ✓.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 18:00:31 +03:00
Nick ShirokovandClaude Opus 5 0fc2d738a2 fix(meta-compile): порядок стандартных реквизитов ещё четырёх типов + Use у ПВХ
Раундтрип покрывал пять видов объектов из сорока с лишним, поэтому расхождения
порядка в остальных были не видны. Списки расширены до «по три объекта из
каждого вида» (112 объектов, 42 вида) — содержательных расхождений нет нигде,
но всплыли перестановки.

1. Порядок стандартных реквизитов, снят с корпуса acc+erp:
   - ChartOfCharacteristicTypes: PredefinedDataName, ValueType, Description,
     Code, IsFolder, Parent, Predefined, DeletionMark, Ref;
   - BusinessProcess: Started, HeadTask, Completed, Ref, DeletionMark, Date, Number;
   - Task: Executed, Description, RoutePoint, BusinessProcess, Ref, DeletionMark,
     Date, Number;
   - AccountingRegister: Account, Active, LineNumber, Recorder, Period.
   IsFolder добавлен в фикс-список ПВХ: он есть во всех 47 объектных блоках
   корпуса (5 выгрузок) и НЕ связан с иерархичностью — плоских ПВХ вдвое больше
   иерархических (36 против 15), и у них он тоже есть.

2. Новый контекст реквизита `cct` для ПВХ. Структурно реквизит ПВХ совпадает со
   справочником, но <Use> у него идёт ПОСЛЕ <Indexing>, а у справочника — перед
   (корпус: Catalog `Use,Indexing,FullTextSearch`, ПВХ
   `Indexing,Use,FullTextSearch,DataHistory`). Раньше оба шли одним контекстом.

Проверка:
- ПВХ (24 объекта): порядок ≠ 23 → 0; все виды (112): 7 → 1;
- шесть основных списков (4897 объектов) без регрессии: порядок ≠ 0,
  TOTAL diff lines 0, match не просел;
- юнит-тесты 648/648 ps1, 645/648 py;
- 1С-сертификация на 8.3.24: accounting-register, business-process, task,
  chart-of-characteristic-types — все приняты;
- дрейф эталонов 6 файлов: перестановки плюс единственное новое содержимое —
  блок IsFolder (проверено вырезанием: без него мультимножества совпадают).

Остаётся известным: у бухрегистра условные стандартные реквизиты стоят по обе
стороны фикс-списка (PeriodAdjustment перед, ExtDimension1..3 и
ExtDimensionType1..3 после), а эмиттер кладёт все «лишние» ключи вперёд. Это
правка механизма, а не таблицы — отдельной задачей.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:37:25 +03:00
Nick ShirokovandClaude Opus 5 0a2dba33cf fix(meta-compile): в Predefined.xml ссылочный тип остаётся локальным
Регрессия предыдущего коммита: cfg: применялся безусловно, а шапка
Ext/Predefined.xml объявляет только predef/v8/xr/xs/xsi — cfg там нет.
Получался неразрешимый префикс, и платформа отказывалась грузить конфигурацию
(1С-сертификация meta-compile/chart-of-characteristic-types: FAIL на
LoadConfigFromFiles; на коде до правки кейс проходил).

Правило платформы, снятое с корпуса, ровно то же, что уже заложено в meta-edit:
префикс из корня, если он там объявлен, иначе локальное объявление. В самом
Predefined.xml платформа пишет
`<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты</v8:Type>`.

Типизированные предопределённые есть только у ПВХ, а ПВХ не входит ни в один
из шести списков раундтрипа — поэтому корпус эту поломку поймать не мог, её
поймала только 1С-сертификация. Добавлен список list-cct-all.txt (24 объекта
acc+erp), чтобы пробел закрылся: content match 24/24, TOTAL diff 0.

Проверка: 1С-сертификация кейса проходит, runner 648/648 ps1 и 645/648 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 16:50:45 +03:00
Nick ShirokovandClaude Opus 5 683e370938 fix(meta-compile,meta-edit): ссылочный тип корневым cfg:, как пишет платформа
Эмитился локальный xmlns:d5p1 на тот же URI, что уже объявлен в шапке файла.
Формально эквивалентно (значим URI, а не префикс), платформа принимала — но
первый же цикл «загрузить в базу → выгрузить» переписывал КАЖДЫЙ ссылочный тип
в cfg:, то есть давал diff-шум при неизменной семантике. Форма пришла из СКД,
где cfg: действительно не работает; в метаданных такого ограничения нет.

Радиус оказался втрое меньше прежней оценки: form-compile в него НЕ входил —
он уже писал cfg:, а все его d5p1 относятся к чужим пространствам
(txtedt/chart/geo/graphscheme/data-analysis), которых в корне нет и где
локальное объявление законно.

meta-edit берёт префикс из объявлений КОРНЯ правимого файла, а если URI там не
объявлен — остаётся на самодостаточной локальной форме. Это не перестраховка:
ссылочный тип живёт в ТЕКСТЕ узла, поэтому XML-слой про префикс не знает и сам
объявление не добавит — проверено, .NET на документе без xmlns:cfg молча пишет
неразрешимый префикс. Та же природа объясняет удалённый костыль в meta-edit.py:
lxml выбрасывал xmlns:d5p1 как «неиспользуемый», и его возвращали регуляркой
после сериализации.

Синхронно снята нормализация ref-префикса в раундтрип-харнесе (debug/, вне git):
она сводила cfg: и d5p1: к одному REF: и делала харнес слепым ровно к тому,
ради чего затевался переход.

Проверка:
- живой цикл через базу (db-create → db-load-xml → db-dump-xml): выгрузка
  платформы совпала с нашими исходниками ПОСТРОЧНО, расхождений ноль;
- корпусный раундтрип 4897 объектов уже БЕЗ маски префикса: match не просел
  (150/1640/151/2600/314/42), порядок ≠ 0, TOTAL diff lines 0;
- юнит-тесты 648/648 ps1, 645/648 py;
- 1С-сертификация meta-edit 17/17 на платформе 8.3.24;
- дрейф эталонов 48 файлов, в диффе нет ни одной строки кроме d5p1: → cfg:;
  skd-* не затронуты ни одним файлом (там cfg: не работает).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 16:28:47 +03:00
Nick ShirokovandClaude Opus 5 a3aa8fe87c fix(meta-compile): порядок элементов по канону выгрузки
Детект порядка, добавленный в раундтрип-харнес, показал 1242 расхождения на
4897 объектах (25%). Раньше они были невидимы: дифф строился через
Compare-Object, то есть по мультимножеству строк, и позиция не проверялась.
Ложных срабатываний там нет по построению — проверка включается только когда
мультимножества уже совпали.

Три категории, все — порядок эмиссии; DSL и декомпилятор не затронуты.

1. Виды детей регистров (1140 объектов). Компилятор печатал
   Resource, Dimension, Attribute. Канон, снятый с корпуса acc+erp (разброса
   внутри типа нет): Resource, Attribute, Dimension у информационного,
   накопления и расчёта; Dimension, Resource, Attribute — у бухгалтерского.
   Команды у платформы идут последними, как и было.

2. Квалификаторы в составном типе (61 объект). Платформа пишет сначала ВСЕ
   <v8:Type>/<v8:TypeSet>, потом блоки квалификаторов, а Emit-TypeContent
   рекурсивно печатал каждую часть целиком. На одиночном типе оба порядка
   совпадают — потому и не всплывало. Порядок самих блоков тоже канонический
   и НЕ зеркалит порядок типов: Number, String, Date (при типах
   boolean,string,dateTime,decimal квалификаторы идут Number,String,Date;
   контрпримеров в корпусе нет).

3. Стандартные реквизиты плана обмена (41 объект, весь список): блок
   начинается с ThisNode, а не с Ref.

Проверка:
- корпусный раундтрип 4897 объектов: порядок ≠ 1242 → 0, match не просел
  (150/1640/151/2600/314/42), TOTAL diff lines 0;
- юнит-тесты 648/648 ps1, 645/648 py;
- 1С-сертификация на платформе 8.3.24: регистры информационный, накопления,
  бухгалтерский, расчёта и план обмена — все приняты;
- дрейф снэпшотов (10 файлов, 9 кейсов) сверен как чистая перестановка строк.
  NB: сверять нужно с нормализацией UUID-\d+ — раннер нумерует плейсхолдеры по
  порядку появления, и перестановка детей меняет, кому достанется UUID-015.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:22:18 +03:00
Nick ShirokovandClaude Opus 5 4833c3b707 fix(mxl-compile): PS-порт не создавал каталог назначения
Наполнение макета по свежему пути (Templates/<Имя>/Ext/Template.xml) на
PowerShell падало с «Could not find a part of the path». py-порт каталог
создаёт, form-compile и skd-compile — тоже; не создавал только этот порт.

Тестами не ловилось: во всех кейсах mxl-compile писал в уже существующий
каталог. Добавлен кейс с записью по несуществующему пути — проверено, что на
коде до правки он падает.

Найдено при разборе вопроса, нужна ли заглушка тела макета в template-add:
оказалось, заглушка держала не замысел, а этот дефект.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:11:11 +03:00
Nick ShirokovandClaude Opus 5 a7406d0982 test(8 навыков): байтовые проверки канона там, где их не было (#57)
Из ~46 навыков-эмиттеров XML восемь не имели ни одной проверки `preserves`:
form-edit, form-remove, meta-remove, skd-edit, template-remove, xdto-edit,
support-edit, cfe-patch-method. Шесть из них правились в предыдущем коммите,
и канон у них держался на том, что правка была механической, а не на проверке.

Ожидания писались ПО КАНОНУ, а не по текущему поведению — и это сразу вскрыло
три дефекта:

1. form-remove очищал слот формы через InnerText="" / text="" — получалась
   пустая пара <DefaultObjectForm></DefaultObjectForm>. Платформа пустых пар
   не пишет (0 на 476 942 XML). Теперь IsEmpty / text=None.

2. Опустевший <ChildObjects> оставался парой, разнесённой по строкам, —
   в PS-порту form-remove и template-remove. Корпус: 1394 самозакрывающихся
   <ChildObjects/> на acc+erp, пустых пар 0 в обеих формах.

3. template-add писал пустой макет парой <SpreadsheetDocument></...>.
   Во всей выгрузке acc_8.3.27 (65 040 XML) многострочных пустых пар нет
   ни для одного тега.

Заодно усилена сама проверка noEmptyPairs: она ловила только СМЕЖНЫЕ теги,
поэтому дефект №2 проходил мимо неё. Добавлен вариант с переводом строки
внутри; дискриминатором служит сам перевод строки — значащий пробельный
текст-узел (<xr:FillValue xsi:type="xs:string">   </xr:FillValue>) его не
содержит и под проверку не попадает.

support-edit покрыт частично (BOM у ParentConfigurations.bin — проверено, что
платформа пишет его с BOM во всех трёх выгрузках), cfe-patch-method — BOM+EOL
у .bsl: хвостовой перевод строки у модулей неканоничен (1235 с ним, 766 без),
поэтому не утверждается.

Регресс: 647/647 ps1, 644/647 py (3 skipped). Эталоны переснятые: три места,
каждое — ровно ожидаемая пара строк.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 13:05:27 +03:00
Nick ShirokovandClaude Opus 5 a3f2a18dbf test(runner): кейсы round-trip на LF-исходнике (#57)
Все 23 проверки preserves.eol ожидали crlf — ни одного LF-исходника в наборе
не было, поэтому расхождение портов по переводам строк жило целиком в
непокрытой ветке и не могло быть замечено.

Добавлены LF-фикстуры и кейсы roundtrip-lf-preserve:
- cf-edit, meta-edit, subsystem-edit — правка существующего файла через
  XmlWriter (наследование стиля исходника);
- role-compile, meta-compile, xdto-compile — регистрация в существующем
  Configuration.xml. Эти кейсы проверяют обе половины правила разом: правка
  наследует LF, а создаваемый рядом файл получает канон CRLF.

Проверено в требуемом порядке: на коде ДО правки кейсы падают
(role-compile/meta-compile/xdto-compile — «expected lf, got 251 CRLF»,
meta-edit — «40 CRLF + 136 lone LF», subsystem-edit — «1 CRLF + 24 lone LF»),
после правки проходят на обоих рантаймах.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 21:42:08 +03:00
Nick ShirokovandClaude Opus 5 3ef0d74158 fix(19 навыков): единая финализация XML — стиль исходника и корректный CDATA (#57)
Три остатка волны #57, все в одной точке — блоке записи XML.

1. Порты расходились по EOL. PS гонял документ через XmlWriter с дефолтным
   NewLineHandling.Replace и принудительно переводил весь файл в CRLF, а py
   сохранял стиль исходника: role-compile на LF-ном Configuration.xml давал
   10311 байт против 10062. Ещё в пяти навыках None стоял, но финальной
   нормализации не было, и на LF-исходнике выходил смешанный EOL (meta-edit:
   40 CRLF + 136 одиночных LF) — та же форма, что у исходного дефекта #57.
   Приведено к форме cf-edit.ps1: None + канонизация к LF + целевой перевод
   строки (стиль файла-назначения, для создаваемого файла — канон CRLF).
   Replace не годится как замена: он превращает переводы строк внутри значений
   атрибутов в &#13;.

2. Гард `if (-notmatch CDATA)` не обрабатывал CDATA, а отказывался от канона
   во всём файле — то есть деградировал до «не сделал ничего». Заменён
   альтернацией: участки CDATA и комментариев возвращаются как есть, замена
   идёт только вне них. На реальных данных поведение не меняется — в корпусе
   из 476 942 XML нет ни одного CDATA и ни одного комментария.

3. py: три реализации одного правила детекта EOL сведены к одной. Мажоритарное
   правило в role-compile/subsystem-compile давало ДРУГОЙ ответ на смешанном
   входе. Дефолты _finalize_xml_bytes для нового файла приведены к канону
   (UTF-8, CRLF, без хвостового перевода); meta-edit срезает хвост в обеих
   ветках, а не только при создании файла.

Попутно, найдено байтовой сверкой портов:
- py вставлял <Role>/<Form>/<Subsystem> с пятью табами вместо трёх —
  подстановка по голому </ChildObjects> удваивала отступ строки; снэпшоты
  этого не видели, так как схлопывают пробелы между тегами;
- py-порты xdto-* писали XML-декларацию одинарными кавычками (так отдаёт
  lxml), платформа и PS пишут двойные.

Регресс: runner 647/647 ps1, 644/647 py (3 skipped), дрейфа снэпшотов нет.
Корпусный раундтрип метаданных: 4897 объектов, match 100%, совпадает с
эталонами захода #57.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 21:41:55 +03:00
Nick ShirokovandClaude Opus 5 750ba3bb13 refactor(эмиттеры): одна формулировка на повторяющийся комментарий
Код нормализации пустого тега повторён в 24 местах, а комментарий к нему за время
работы над #57 расплодился в шесть вариантов формулировки. Это то же расхождение
копий, что и в коде, только в тексте: читающий не понимает, значима разница или нет.

Сведено к одной паре формулировок — про сам тег с гардом и про запись через
MemoryStream. Комментарий теперь начинается с ПРАВИЛА, а не с причины: раньше
верхняя строка рассказывала, что here-string наследует LF от .ps1, и читалось это
как «переводы строк берём из скрипта навыка» — тогда как правило обратное:
создаём файл — пишем канон, правим существующий — наследуем его стиль.

Два пояснения в cfe-borrow унификация чуть не стёрла, они возвращены: источником
там служит OuterXml, а не XmlWriter, и нормализация стоит после вставки реквизитов,
чтобы накрыть и их. Это причина местного отличия, а не дубль.

Ссылки на ишью прорежены: #44/#46/#47 осталась по одной на файл — там, где правило
формулируется, — и убрана оттуда, где была эхом. #57 в скриптах не было вовсе.

Заодно выровнены две мелочи, найденные той же сверкой копий: interface-edit в
py-порте не обрезал хвостовой перевод там, где PS обрезает (поведение совпадало,
код — нет), а cf-edit нормализовал EOL инлайном вместо общей двухшаговой формы.

Версии подняты в ОБОИХ портах всех затронутых навыков, включая те, где текстуально
менялся только .ps1: номер версии в этом проекте читается как «одно и то же
поведение», и разводить его из-за комментария нельзя. Паритет проверен — 69 пар,
ноль расхождений.

Windows 641/641 обоими портами.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:06:38 +03:00
Nick ShirokovandClaude Opus 5 4c64b97cec fix(skd-edit): гард на CDATA при нормализации пустого тега
skd-edit нормализовал ` />` в `/>` без проверки на CDATA и комментарии, в отличие
от остальных 23 мест, где такая нормализация есть. Вскрылось сверкой копий:
конструкция повторяется в 24 местах, и вариантов формы должно быть ровно два —
с гардом и никак иначе.

Гард нужен потому, что `>` не экранируется только внутри CDATA и комментариев —
там ` />` может оказаться содержимым, а не концом тега. В 1С-метаданных ни того,
ни другого не встречается (0 из 476 943 XML корпуса), так что практического
расхождения поведения нет — но полагаться на это в одном месте и защищаться
в остальных двадцати трёх непоследовательно.

Правка в обоих портах: у py-порта нормализация тоже была без гарда (она там
защитная — lxml и так пишет плотно).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:05:47 +03:00
Nick ShirokovandClaude Opus 5 67185657c1 refactor(эмиттеры): копии канон-writer'а приведены к одному виду
Навыки автономны, общей библиотеки нет — значит копии функции записи должны быть
буквально одинаковыми, иначе теряется смысл копипасты. PS-порт дисциплину держал
(7 копий байт-в-байт), а py разъехались: три варианта имени параметра, два стиля
кавычек, у cfe-borrow ещё и другое имя функции (save_xml_file).

Сведено к одной форме: имя write_xml_file, параметр content, одинаковое тело.
Различаются ровно две вещи, и обе осмысленные: вызов локального базового writer'а
(имена исторические — write_utf8_bom / write_text_with_bom / save_text_bom) и одна
строка примечания там, где у навыка есть исключение (модуль .bsl, HTML).

Текст комментария в PS выровнен с docstring'ом py: раньше он описывал ПРИЧИНУ
(here-string наследует LF), а не правило, и после правки py ports разъехались по
смыслу. Синтаксис остаётся разным по природе языков: в py docstring (в портах их
421 против 73 функций с #-комментарием — это местная норма), в PS — #.

Windows 641/641 обоими портами.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:22:43 +03:00
Nick ShirokovandClaude Opus 5 d05c91be03 fix(epf-init,erf-init,form-add): CRLF в разделителях строк модуля
Скелетные генераторы писали Module.bsl с LF, а платформа пишет модули с CRLF:
в выгрузке БП 2643 CRLF и чисто-LF 0 из 3001 модуля (358 «смешанных» — LF внутри
строковых литералов кода), BOM 2001/2001. По тому же доводу сходимости, что и для
XML: платформа перепишет модуль в CRLF при первой выгрузке, значит наш LF — дифф,
созданный нами.

В репозитории уже была встречная конвенция: meta-compile (модуль команды) и
cfe-patch-method (base/local/remote.bsl) пишут .bsl явным CRLF в обоих портах.
Выбивались только эти три скелета.

Хвостовой перевод строки НЕ трогаем: у платформы он неканоничен — 1235 модулей
с ним, 766 без; это след автора кода, а не правило. Меняем только разделители.

Правка в точке записи, а не в шаблоне: так одна строка на скрипт и не трогаются
\u-экранированные кириллические литералы в py (Edit по ним переэкранирует).

Снэпшоты нормализуют EOL и увидеть это не могут — добавлен preserves на модуль
в epf-init/basic, erf-init/basic, form-add/basic.

Windows 641/641 обоими портами, мак 590/0/51, дрейфа снэпшотов нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:53:50 +03:00
Nick ShirokovandClaude Opus 5 caf038a192 fix(cfe-borrow,template-add,help-add,interface-edit): вывод py-порта не зависит от ОС (#57)
Тот же writer без newline="", что чинился в form-add.py, остался ещё в четырёх
py-портах, и пишут они не модули, а 1С XML: заимствованные объекты и метаданные
формы (cfe-borrow), Template.xml и XML-макеты (template-add), Help.xml (help-add),
вновь создаваемый CommandInterface.xml (interface-edit).

Контент там собирается литералами с \n, поэтому текстовый режим Python давал CRLF
на Windows — «случайно правильно» — и LF на macOS, ломая канон #57 ровно там, где он
только что установлен. Проверено на маке ДО фикса: cfe-borrow 0 CRLF + 33 одиночных
LF, template-add 0 + 2, interface-edit 0 + 5. Порчи \r\r\n не было, но это везение:
подай такой writer CRLF-контент — каждая строка стала бы \r\r\n.

Почему не поймали раньше: аудит гонялся на Windows, где текстовый режим даёт канон;
снэпшоты нормализуют EOL; байтовый preserves ни разу не проверялся на маке.

Разделены два пути, которые я сперва смешал в cfe-borrow:

- save_xml_file — канон (CRLF, без хвоста) для файлов, которые СОЗДАЁМ;
- save_text_bom — пишет как есть, для файлов, которые ПРАВИМ: переводы строк уже
  пришли из самого файла и менять их нельзя (контракт #44/#46/#47). Туда же
  добавлено чтение с newline="" — иначе CRLF терялся ещё на входе.

interface-edit чинится и в PS-порте: ветка -CreateIfMissing давала смешанный EOL
(3 CRLF + 2 одиночных LF) уже на Windows. Аудит #57 её пропустил, потому что
фильтровал файлы по расширению .xml, а кейс создаёт файл с именем без расширения.

Канон, как выяснилось, зависит от типа артефакта: XML — CRLF, текстовый макет —
CRLF, а HTML-макет платформа хранит с LF (корпус: 399 LF из 400). Поэтому HTML
через канон-writer НЕ идёт.

preserves добавлен четырём навыкам (у них его не было вовсе) + отдельно на Help.xml.
Именно прогон на маке и даёт покрытие: на Windows эти кейсы зелены и без фикса.

Windows 641/641 обоими портами, мак 590/0/51, дрейфа снэпшотов нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:53:17 +03:00
Nick ShirokovandClaude Opus 5 668b145010 docs(form-remove,help-add,template-add,template-remove): единое имя навыка в шапках обоих портов
В .ps1 шапка называла НАВЫК, в .py — ФАЙЛ (help-add v1.12 против add-help v1.12).
Канон — имя навыка: так называется каталог, так навык вызывается, так он записан
в SKILL.md. Расхождение было только у этих четырёх: у них имя файла отличается от
имени навыка. Версии совпадали, поменялось только имя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:29:54 +03:00
Nick ShirokovandClaude Opus 5 d7fd6d79d3 test(runner): байтовые проверки канона вместо масок (#57)
Раннер сам прятал дефекты, которые чинит #57. normalizeXmlContent при
runtime=python снимал ровно четыре измерения: пробел перед `/>`, whitespace между
тегами, пустую пару <Tag></Tag> и хвостовой пробельный мусор. Паритет PS<->PY по
этим измерениям проверялся ЧЕРЕЗ маску — расхождение физически не могло упасть.

Снято три из четырёх (пробел, пустая пара, хвост). Схлопывание whitespace между
тегами оставлено: порты кое-где расставляют отступы иначе, это форматирование, а
не байтовый канон.

Ужесточён checkPreserves: eol теперь считает ОДИНОЧНЫЕ LF, а не «есть ли хоть один
CR». Прежняя проверка пропускала смешанный выход — cfe-init давал 10 CR на 70
строк и проходил её, то есть головной дефект тикета был ей невидим. Добавлены
ключи selfClose:"tight" и noEmptyPairs; preserves проставлен в 13 кейсах
навыков-эмиттеров (по одному на навык, на его СОБСТВЕННЫЙ артефакт).

Снятие масок сразу вскрыло четыре реальных расхождения портов:

- form-edit собирает выход из OuterXml и писал `<a />`; в списке 17 навыков его не
  было, потому что искал по вызовам Save — здесь другой путь. Тот же случай, что
  с Form.xml в cfe-borrow;
- meta-edit py дописывал хвостовой перевод в создаваемый Ext/Predefined.xml и
  читал существующий без newline='' (терял CRLF);
- skd-info py писал отчёт -OutFile без хвостового перевода, PS через WriteAllLines
  — с ним. Это текстовый отчёт, канон Конфигуратора к нему не относится, поэтому
  выровнял py по существующему эталону;
- фикстуры кейсов содержали <Vendor></Vendor> — снимок нашего же старого вывода.
  Конфигуратор пустых пар не пишет, .NET их сохраняет, lxml схлопывает. Поправлены
  10 фикстур: пары → самозакрывающиеся, EOL и BOM не тронуты.

Результат: PS 641/641, python 638/641 (+3 runtimeOnly-скипа) — со снятыми масками.
Дрейф снэпшотов: 10 файлов, только пробельные теги и пустые пары.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:23:13 +03:00
Nick ShirokovandClaude Opus 5 11e4e4301f fix(cf-init,cfe-init,meta-compile): пустой элемент вместо пустой пары (#57)
Конфигуратор пишет <Vendor/>, а не <Vendor></Vendor>: в 8 выгрузках cfsrc
(476 943 XML) пустых пар ноль. Шаблоны подставляли значение ВНУТРЬ пары, поэтому
при пустом значении получалась пара.

Собираем элемент целиком: <Vendor>/<Version> (cf-init, cfe-init),
<DefaultRoles> (cfe-init при -NoRole), <Key> и <Namespace> (meta-compile —
план обмена и веб-сервис). Образец рядом же: <Comment> и <Description> в
meta-compile так делали и раньше.

Дефект был в ОБОИХ портах одинаково — здесь дело не в сериализаторе, а в шаблоне.

Пустая пара <Namespace> в cfe-borrow была не его: он копирует свойства
заимствуемого объекта, а порождал их meta-compile.

Дрейф снэпшотов: 340 <Vendor/>, 339 <Version/>, по одному <Key/>, <Namespace/>,
<DefaultRoles/>. Содержательных изменений ноль. Тесты 641/641.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:55:31 +03:00
Nick ShirokovandClaude Opus 5 0b8231c467 fix(17 навыков): CRLF в разделителях строк XML (#57)
Головной дефект тикета: cfe-init выдавал Configuration.xml с 10 CR на 70 строк, а
роль и язык — вовсе без CR. Теперь 70/70, последний байт `>`.

Правило: меняем только РАЗДЕЛИТЕЛИ строк, содержимое текстовых узлов не трогаем.
Платформа его не трогает тоже — в запросах СКД из чистой выгрузки встречаются и
CRLF, и одиночный LF. Поэтому сплошная нормализация файла применяется только там,
где многострочных текстовых узлов нет по построению (скелеты), а в объектном XML
и формах разделители правятся в точке сборки.

Источников оказалось четыре, а не один:

1. Скелеты (cf-init, cfe-init, epf-init, erf-init, form-add, help-add,
   template-add) собираются here-string'ами, а .ps1/.py в репозитории хранятся с
   LF — отсюда LF и смешанный EOL.
2. py-порты склеивали документ через '\n'.join(lines); PS в тех же местах давал
   CRLF через AppendLine — порты расходились побайтово.
3. Билдеры Predefined в meta-compile собирались с явным LF и даже сворачивали
   CRLF→LF из общего эмиттера типов.
4. Чтение существующего файла в python БЕЗ newline='' молча схлопывает CRLF в LF
   (универсальные переводы строк), и запись потом кладёт LF. Так role-compile и
   subsystem-compile переписывали в LF весь Configuration.xml. meta-compile это
   уже делал правильно — там newline='' стоял с фикса #44/#46/#47.

Отдельно: XML-парсер по спецификации схлопывает CRLF при разборе, поэтому
lxml-порты (xdto-compile, xdto-edit) отдавали LF-документ там, где .NET возвращал
CRLF через NewLineHandling. Восстанавливаем EOL исходного файла.

Разделение канона и сохранения стиля:

- файл СОЗДАЁМ — канон (CRLF, без хвоста);
- существующий ПРАВИМ — наследуем его EOL, включая перевод строки вставки
  (контракт #44/#46/#47). Иначе LF-проект получал бы смешанные файлы — ровно то,
  на что заведён #57. Кейсы roundtrip-crlf-preserve остаются зелёными.

Хелпер записи скопирован в каждый навык (навыки автономны). У meta-compile он
называется Write-XmlFileKeepEol / write_xml_file_keep_eol: там нормализовать EOL
НЕЛЬЗЯ (многострочные запрос, синоним, значение заполнения), и одинаковое имя при
разном поведении было бы ловушкой.

Заодно: имя временного файла батча в meta-compile.ps1 получило GUID. Фиксированное
"meta-compile-batch-$idx.json" в общем %TEMP% сталкивало два параллельных запуска
(«file is being used by another process») — из-за этого полный набор приходилось
гонять с урезанной параллельностью. py-порт уже брал mkstemp.

Аудит: было ~250 файлов с дефектом EOL, стало 0 в обоих портах. Осталось четыре
законных случая — фикстуры roundtrip-crlf-preserve (сохранение стиля) и запрос
динсписка с многострочным текстом. Дрейф снэпшотов — 1 файл. Тесты 641/641.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:37:26 +03:00
Nick ShirokovandClaude Opus 5 abe9431d8f fix(9 навыков): без хвостового перевода строки в конце XML (#57)
Конфигуратор не пишет перевод строки в конце файла — последний байт `>`. Это видно
на чистой выгрузке пустой ИБ (Windows и macOS) и на всех 8 выгрузках в cfsrc.
Наши эмиттеры добавляли лишний, причём оба порта и по разным причинам:

- PS: StringBuilder.AppendLine дописывает Environment.NewLine после последней строки;
- PY: '\n'.join(lines) + '\n' — хвост добавлен явно.

В meta-compile заведена единая точка записи Write-XmlFile: там документ собирают
и AppendLine, и явные `n в билдерах Predefined/Content/Flowchart, а выходов семь.
В остальных обрезка стоит в самом вызове записи.

Модули .bsl, JSON-выход form-compile, HTML-справка help-add и XSD из xdto-decompile
сюда НЕ входят: канон Конфигуратора описывает XML метаданных, а у этих артефактов
свои правила. Результаты XmlDocument.Save тоже не трогаются — у них хвоста нет.

Навыки редактирования продолжают сохранять стиль ВХОДНОГО файла: кейс
subsystem-edit/roundtrip-crlf-preserve, где фикстура намеренно с хвостом, остаётся
зелёным. Это контракт #44/#46/#47, и он не отменяется.

Дрейф снэпшотов: 547 файлов, у каждого только последняя строка, содержательных
изменений ноль. Тесты 641/641.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:53:05 +03:00
Nick ShirokovandClaude Opus 5 39cf8e49cf fix(17 навыков): самозакрывающийся тег без пробела и UTF-8 в декларации (#57)
.NET XmlWriter пишет `<a />`, а Конфигуратор — `<a/>`. Поскольку Save переписывает
файл целиком, meta-edit при добавлении одного реквизита переводил в пробельную форму
все пустые теги документа. Дефект только у PS-порта: lxml (45 из 49 py-портов) уже
пишет плотно, то есть порты расходились побайтово.

Канон измерен, а не предположен: чистая выгрузка пустой ИБ на Windows и macOS плюс
сплошной скан 8 выгрузок в cfsrc — 476 943 XML, 21 294 119 самозакрывающихся тегов,
пробельных 0. Форма не зависит от ОС, версии платформы (8.3.20-8.5) и наличия
атрибутов. Правило уже было реализовано в skd-edit — оттуда и взято.

Замена безопасна доказуемо: .NET экранирует `>` как `&gt;` и в тексте, и в атрибутах,
поэтому ` />` после Save — только конец тега. Лазейки (CDATA, комментарии) в
1С-метаданных не встречаются — 0 из 476 943 файлов; гард на них всё равно стоит.

Одиннадцать скриптов писали прямо в FileStream без пост-обработки — переведены на
MemoryStream, что заодно чинит `encoding="utf-8"` строчными (335 файлов в снэпшотах).
В cfe-borrow правится и сборка Form.xml: куски берутся из OuterXml, а он спацовывает
так же, как XmlWriter.

Дрейф снэпшотов: 12 838 строк тегов + 335 деклараций, содержательных изменений ноль.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:28:58 +03:00
Nick ShirokovandClaude Opus 5 ccd860ae89 docs(tests): эталон при preRun фиксирует вход теста
README называл info/validate-навыки типичным случаем для noSnapshot: эталон
зафиксировал бы выход preRun, а не проверяемого навыка. Факт верный, вывод — нет.

Когда preRun собирает фикстуру, эталон фиксирует ВХОД теста. Без него дрейф
навыка-генератора меняет фикстуру молча, и ожидание вроде «Составной (6)»
начинает проверяться на другом объекте — либо падает без внятной причины, либо
сходится случайно и перестаёт что-либо проверять. meta-compile такой дрейф даёт
регулярно. Поэтому ни один из семи info-навыков noSnapshot не использует —
у всех эталоны, и это осознанно, а не упущение.

Правило сформулировано явно: есть preRun с генерацией фикстуры → эталон;
нет preRun или фикстура тривиальна → noSnapshot.

В шапке verify-snapshots отмечено, что он работает и с кейсами навыков, которые
сами ничего не пишут, — там он проверяет, что платформа принимает собранную
preRun фикстуру. Плюс предупреждение, что на кейсах setup: external проверка
вырождается в загрузку 1С собственной выгрузки: ~3 минуты на кейс без пользы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:13:58 +03:00
Nick ShirokovandClaude Opus 5 5141cf546d fix(meta-info): иерархия не только у справочника
Строка «Иерархический: …» печаталась в ветке Catalog, хотя свойство
Hierarchical есть и у планов видов характеристик — в корпусе acc/erp/ut/unf
таких 13. У них иерархия не выводилась ни в overview, ни в full.

Хуже того, вывод противоречил сам себе: блок стандартных реквизитов в full
показывал Родитель (он определяется по тому же Hierarchical), а строки про
иерархию рядом не было — родитель появлялся ниоткуда.

Иерархия вынесена из ветки Catalog и печатается по наличию свойства;
подчинение осталось специфичным для справочника. Отсутствие иерархии
по-прежнему не печатается: сообщаем о наличии, молчим об отсутствии —
тихую ошибку в коде даёт только необнаруженное наличие.

Свойство Hierarchical проверено по корпусу: оно есть только у Catalog и
ChartOfCharacteristicTypes (у плана счетов иерархия по природе, свойства нет).
Прогон по корпусу: ровно 13 изменённых строк, побочных эффектов нет.

Находка из отклонённого PR #59 — там иерархия тоже была вынесена из ветки
Catalog. Реализация не заимствована: PR печатал «Иерархический: нет» для
неиерархических объектов, то есть строку-шум примерно у 4000 объектов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:54:43 +03:00
Nick ShirokovandClaude Opus 5 66807e02c7 test(meta-info): кроссплатформенные кейсы на свёртку ТипЗначения
Свёртку составного типа и её разворот через drill-down проверял только кейс
на выгрузке acc_8.3.24, а он привязан к локальному пути и на macOS
пропускается — то есть ключевая новая логика там не проверялась вовсе.

Добавлены два синтетических кейса на фикстуре от meta-compile: ПВХ с шестью
типами сворачивается в «Составной (6)» и печатает подсказку, а -Name
ТипЗначения разворачивает список. В составе фикстуры два v8:TypeSet
(cfg:CatalogRef, cfg:DocumentRef), поэтому кейсы заодно покрывают разбор
обобщённых ссылочных типов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:09:52 +03:00
Nick ShirokovandClaude Opus 5 50f8b497b6 feat(meta-info): блок стандартных реквизитов
Ишью #58: сводка не показывала владельца подчинённого справочника, и код,
написанный по ней, падал на Записать(). Владелец оказался частным случаем:
шапка «Код(9) | Наименование(100)» смешивала свойства объекта с полями и не
давала по полям ни типа, ни обязательности. По acc_8.3.24 из 690 справочников
23 имеют числовой код, 122 — фиксированной длины, 426 — вовсе без кода; всё
это было невидимо. У ПВХ, планов счетов и обмена, бизнес-процессов и задач
шапки не было вообще — включая ТипЗначения у ПВХ.

Поля вынесены в блок «Стандартные реквизиты» на языке блока «Реквизиты»
(имя — тип — [флаги]), в шапке остались свойства объекта. В overview
показываются те, что нельзя вывести из остального вывода: Владелец, Код,
Наименование, Дата, Номер, ТипЗначения. Ссылка, ПометкаУдаления, Родитель,
ЭтоГруппа следуют из типа объекта и строки «Иерархический» — они в full.

Обязательность берётся из StandardAttributes/FillChecking. Блок в XML
опционален (meta-dsl-spec §7.1.1), поэтому при его отсутствии действует
профиль платформенных дефолтов, синхронный с meta-compile — иначе объекты,
созданные meta-compile, теряли бы флаг, то есть ровно кейс из ишью.

Составной ТипЗначения ПВХ доходит до 151 типа и сворачивается в счётчик
«Составной (110)»; полный состав даёт drill-down -Name ТипЗначения, о чём
сообщает единственная строка в конце вывода. brief получил строку
«Подчинён: …» — факт структуры без обещаний про обязательность, которых
brief выполнить не может.

Проверено сравнением с эталонным прогоном по 4768 объектам acc/erp/ut/unf:
все расхождения классифицированы, необъяснённых нет, ненулевых кодов возврата
нет. Владелец появился у 242 объектов, числовой код — у 56.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:57:32 +03:00
Nick ShirokovandClaude Opus 5 c687860535 fix(meta-info): обобщённые ссылочные типы вместо сырого cfg:
Ветка TypeSet в Format-Type распознавала только DefinedType и Characteristic,
а остальные наборы отдавала в вывод как есть. Голое имя метатипа без «.Имя»
означает все ссылки этого класса (docs/meta-dsl-spec.md), и такие типы
печатались сырыми — 254 вхождения в 82 строках на корпусе acc/erp/ut/unf:

  ЗначениеПустойСсылки   cfg:ExchangePlanRef | cfg:BusinessProcessRef | …
  Свойство1              Булево | Строка(128) | ДатаВремя | cfg:AnyIBRef

Разбор вынесен в Format-SingleTypeSet: AnyRef → ЛюбаяСсылка, AnyIBRef →
ЛюбаяСсылкаИБ, голые XxxRef — через существующий refTypeMap. Конкретный тип
всегда пишется с точкой, поэтому «СправочникСсылка» без точки однозначно
читается как обобщённый.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:52:39 +03:00
Nick ShirokovandClaude Opus 5 cdf0944b2f fix(meta-info): имя команды вместо склеенного XML
Get-SimpleChildren брала InnerText дочернего узла ChildObjects. Для Form и
Template это верно — имя лежит прямо в тексте узла. Command же имеет вложенный
Properties, и InnerText склеивал всё его содержимое в одну строку:

  Команды: БанковскиеСчета_ИнтерфейсИнтеграцииСБанкомruБанковские счетаForm
  NavigationPanelImportantcfg:CatalogRef.ОрганизацииSinglefalseAutoAuto

Теперь имя берётся из Properties/Name, если оно есть, иначе — прежний InnerText.
Задеты обе точки вывода: overview у отчётов и обработок и full у всех типов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:59:30 +03:00
1459 changed files with 48089 additions and 18353 deletions
+17 -3
View File
@@ -1,4 +1,4 @@
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath, [Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
@@ -163,6 +163,8 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
Assert-EditAllowed $resolvedPath 'editable' Assert-EditAllowed $resolvedPath 'editable'
# --- Load XML with PreserveWhitespace --- # --- Load XML with PreserveWhitespace ---
# NB: парсер XML по спецификации схлопывает CRLF в LF, а вставки ниже собираются с
# явным CRLF — поэтому EOL приводится к целевому в точке записи (см. финализацию).
$script:xmlDoc = New-Object System.Xml.XmlDocument $script:xmlDoc = New-Object System.Xml.XmlDocument
$script:xmlDoc.PreserveWhitespace = $true $script:xmlDoc.PreserveWhitespace = $true
$script:xmlDoc.Load($resolvedPath) $script:xmlDoc.Load($resolvedPath)
@@ -691,7 +693,9 @@ $bodyBlock$declarations
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml" $caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom) # Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце.
$caiXml = ($caiXml -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($caiPath, $caiXml.TrimEnd("`r", "`n"), $utf8Bom)
$script:modifyCount++ $script:modifyCount++
Info "Wrote panel layout: $caiPath" Info "Wrote panel layout: $caiPath"
} }
@@ -880,7 +884,9 @@ $rightXml
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
$hpPath = Join-Path $extDir "HomePageWorkArea.xml" $hpPath = Join-Path $extDir "HomePageWorkArea.xml"
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom) # Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце.
$hpXml = ($hpXml -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($hpPath, $hpXml.TrimEnd("`r", "`n"), $utf8Bom)
$script:modifyCount++ $script:modifyCount++
Info "Wrote home page layout: $hpPath" Info "Wrote home page layout: $hpPath"
} }
@@ -982,6 +988,14 @@ $memStream.Close()
$text = [System.Text.Encoding]::UTF8.GetString($bytes) $text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') $text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) [System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
+81 -19
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -12,6 +12,65 @@ import uuid as _uuid
from html import escape as html_escape from html import escape as html_escape
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================ # ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
@@ -341,21 +400,22 @@ def _detect_xml_style(path):
def _finalize_xml_bytes(xml_bytes, style): def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None).""" """Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
enc_decl = style["enc"] if style else "utf-8" выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace( xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>", b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>') b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах) # Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"") xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n")) .replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть) # Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else True want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n") xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl: if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение) # EOL — как в оригинале (новый файл → CRLF, канон #57)
if style and style["crlf"]: if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n") xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes return xml_bytes
@@ -379,7 +439,7 @@ def main():
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"]) parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
parser.add_argument("-Value", default=None) parser.add_argument("-Value", default=None)
parser.add_argument("-NoValidate", action="store_true") parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args() args = ci_parse_args(parser)
if args.DefinitionFile and args.Operation: if args.DefinitionFile and args.Operation:
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr) print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
@@ -762,7 +822,7 @@ def main():
layout = value layout = value
if isinstance(layout, str): if isinstance(layout, str):
try: try:
layout = json.loads(layout) layout = ci_json(json.loads(layout))
except json.JSONDecodeError: except json.JSONDecodeError:
print(f"set-panels value must be valid JSON object", file=sys.stderr) print(f"set-panels value must be valid JSON object", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -917,7 +977,7 @@ def main():
layout = value layout = value
if isinstance(layout, str): if isinstance(layout, str):
try: try:
layout = json.loads(layout) layout = ci_json(json.loads(layout))
except json.JSONDecodeError: except json.JSONDecodeError:
print("set-home-page value must be valid JSON object", file=sys.stderr) print("set-home-page value must be valid JSON object", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -985,7 +1045,7 @@ def main():
if not os.path.isabs(def_file): if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file) def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh: with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = json.loads(fh.read()) ops = ci_json(json.loads(fh.read()))
if isinstance(ops, list): if isinstance(ops, list):
operations = ops operations = ops
else: else:
@@ -995,23 +1055,25 @@ def main():
for op in operations: for op in operations:
op_name = op.get("operation", args.Operation or "") op_name = op.get("operation", args.Operation or "")
# PS сравнивает имя операции через switch, а он регистронезависим.
op_key = str(op_name).lower()
op_value = op.get("value", args.Value or "") op_value = op.get("value", args.Value or "")
if op_name == "modify-property": if op_key == "modify-property":
do_modify_property(op_value if isinstance(op_value, str) else str(op_value)) do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "add-childObject": elif op_key == "add-childobject":
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value)) do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "remove-childObject": elif op_key == "remove-childobject":
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value)) do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "add-defaultRole": elif op_key == "add-defaultrole":
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value)) do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "remove-defaultRole": elif op_key == "remove-defaultrole":
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value)) do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "set-defaultRoles": elif op_key == "set-defaultroles":
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value)) do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "set-panels": elif op_key == "set-panels":
do_set_panels(op_value) do_set_panels(op_value)
elif op_name == "set-home-page": elif op_key == "set-home-page":
do_set_home_page(op_value) do_set_home_page(op_value)
else: else:
print(f"Unknown operation: {op_name}", file=sys.stderr) print(f"Unknown operation: {op_name}", file=sys.stderr)
+1 -1
View File
@@ -1,4 +1,4 @@
# cf-info v1.4 — Compact summary of 1C configuration root # cf-info v1.5 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath, [Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
+24 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-info v1.4 — Compact summary of 1C configuration root # cf-info v1.5 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -12,6 +12,28 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Argument parsing --- # --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False) parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory") parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
@@ -20,7 +42,7 @@ parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, he
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show") 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") parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file") parser.add_argument("-OutFile", default="", help="Write output to file")
args = parser.parse_args() args = ci_parse_args(parser)
# --- Output helper (collect all, paginate at the end) --- # --- Output helper (collect all, paginate at the end) ---
lines_buf = [] lines_buf = []
+64 -17
View File
@@ -1,4 +1,4 @@
# cf-init v1.4 — Create empty 1C configuration scaffold # cf-init v1.11 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -16,6 +16,12 @@ param(
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
function Esc-XmlText {
param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve output dir --- # --- Resolve output dir ---
@@ -43,6 +49,9 @@ $co6 = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString() $co7 = [guid]::NewGuid().ToString()
# --- Mobile functionalities --- # --- Mobile functionalities ---
# Версия формата как число — по ней ниже включаются вставки 2.21.
$is221 = (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221)
$mobileFuncs = @( $mobileFuncs = @(
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"), @("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"), @("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
@@ -59,6 +68,9 @@ $mobileFuncs = @(
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"), @("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false") @("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
) )
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.21 (8.5),
# последней в списке. На младших форматах платформа её не пишет.
if ($is221) { $mobileFuncs += ,@("TextToSpeech","false") }
$mobileXml = "" $mobileXml = ""
foreach ($mf in $mobileFuncs) { foreach ($mf in $mobileFuncs) {
@@ -68,17 +80,43 @@ foreach ($mf in $mobileFuncs) {
# --- Synonym XML --- # --- Synonym XML ---
$synonymXml = "" $synonymXml = ""
if ($Synonym) { if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t" $synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
} }
# --- Optional properties --- # --- Optional properties ---
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } # Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } # пишет <Vendor/>, а не <Vendor></Vendor>.
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
# --- Свойства и пространство имён формата 2.21 (платформа 8.5) ---
# Значения и ПОЗИЦИИ сняты с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники,
# выгруженные с 8.3.27 и с 8.5.1, различаются ровно этим. Порядок важен — вставки идут
# на своё место, а не в конец.
$nl = "`r`n"
$f221AuxForms = ""; $f221WindowVariant = ""; $f221OpenVariant = ""; $f221Captions = ""; $f221Migration = ""
$palNs = ""
if ($is221) {
$palNs = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
# Скобки вокруг -join обязательны: без них `$nl + (массив) -join $nl` разбирается как
# `($nl + массив) -join $nl`, массив склеивается пробелами и все теги уезжают в одну строку.
$f221AuxForms = $nl + ((@(
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"
) | ForEach-Object { "`t`t`t$_" }) -join $nl)
$f221WindowVariant = $nl + "`t`t`t<MainClientApplicationWindowInterfaceVariant>NavigationLeft</MainClientApplicationWindowInterfaceVariant>" +
$nl + "`t`t`t<ClientApplicationTheme>Auto</ClientApplicationTheme>"
$f221OpenVariant = $nl + "`t`t`t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs</ClientApplicationWindowsOpenVariant>"
$f221Captions = $nl + "`t`t`t<Caption/>" + $nl + "`t`t`t<ShortCaption/>"
$f221Migration = $nl + "`t`t`t<Version85InterfaceMigrationMode>DontUse</Version85InterfaceMigrationMode>"
}
# --- Configuration.xml --- # --- Configuration.xml ---
$cfgXml = @" $cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"$palNs xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
<Configuration uuid="$uuidCfg"> <Configuration uuid="$uuidCfg">
<InternalInfo> <InternalInfo>
<xr:ContainedObject> <xr:ContainedObject>
@@ -111,7 +149,7 @@ $cfgXml = @"
</xr:ContainedObject> </xr:ContainedObject>
</InternalInfo> </InternalInfo>
<Properties> <Properties>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name> <Name>$(Esc-XmlText ($Name))</Name>
<Synonym>$synonymXml</Synonym> <Synonym>$synonymXml</Synonym>
<Comment/> <Comment/>
<NamePrefix/> <NamePrefix/>
@@ -122,8 +160,8 @@ $cfgXml = @"
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor>$vendorXml</Vendor> $vendorEl
<Version>$versionXml</Version> $versionEl
<UpdateCatalogAddress/> <UpdateCatalogAddress/>
<IncludeHelpInContents>false</IncludeHelpInContents> <IncludeHelpInContents>false</IncludeHelpInContents>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication> <UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
@@ -145,15 +183,15 @@ $cfgXml = @"
<DefaultDataHistoryChangeHistoryForm/> <DefaultDataHistoryChangeHistoryForm/>
<DefaultDataHistoryVersionDataForm/> <DefaultDataHistoryVersionDataForm/>
<DefaultDataHistoryVersionDifferencesForm/> <DefaultDataHistoryVersionDifferencesForm/>
<DefaultCollaborationSystemUsersChoiceForm/> <DefaultCollaborationSystemUsersChoiceForm/>$f221AuxForms
<RequiredMobileApplicationPermissions/> <RequiredMobileApplicationPermissions/>
<UsedMobileApplicationFunctionalities>$mobileXml <UsedMobileApplicationFunctionalities>$mobileXml
</UsedMobileApplicationFunctionalities> </UsedMobileApplicationFunctionalities>
<StandaloneConfigurationRestrictionRoles/> <StandaloneConfigurationRestrictionRoles/>
<MobileApplicationURLs/> <MobileApplicationURLs/>
<AllowedIncomingShareRequestTypes/> <AllowedIncomingShareRequestTypes/>$f221WindowVariant
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode> <MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>$f221OpenVariant
<DefaultInterface/> <DefaultInterface/>$f221Captions
<DefaultStyle/> <DefaultStyle/>
<DefaultLanguage>Language.Русский</DefaultLanguage> <DefaultLanguage>Language.Русский</DefaultLanguage>
<BriefInformation/> <BriefInformation/>
@@ -165,7 +203,7 @@ $cfgXml = @"
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode> <ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
<ModalityUseMode>DontUse</ModalityUseMode> <ModalityUseMode>DontUse</ModalityUseMode>
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode> <SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode> <InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>$f221Migration
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode> <DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
<CompatibilityMode>$CompatibilityMode</CompatibilityMode> <CompatibilityMode>$CompatibilityMode</CompatibilityMode>
<DefaultConstantsForm/> <DefaultConstantsForm/>
@@ -180,7 +218,7 @@ $cfgXml = @"
# --- Languages/Русский.xml --- # --- Languages/Русский.xml ---
$langXml = @" $langXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"$palNs xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
<Language uuid="$uuidLang"> <Language uuid="$uuidLang">
<Properties> <Properties>
<Name>Русский</Name> <Name>Русский</Name>
@@ -240,11 +278,20 @@ if (-not (Test-Path $extDir)) {
# --- Write files with UTF-8 BOM --- # --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) # XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $cfgFile $cfgXml $enc
$langFile = Join-Path $langDir "Русский.xml" $langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc) Write-XmlFile $langFile $langXml $enc
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml" $caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc) Write-XmlFile $caiFile $caiXml $enc
# --- Output --- # --- Output ---
Write-Host "[OK] Создана конфигурация: $Name" Write-Host "[OK] Создана конфигурация: $Name"
+89 -21
View File
@@ -1,19 +1,55 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-init v1.4 — Create empty 1C configuration scaffold # cf-init v1.11 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration.""" """Generates minimal XML source files for a 1C configuration."""
import sys, os, argparse, uuid import sys, os, argparse, re, uuid
def esc_xml(s): # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;') # регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def new_uuid(): def new_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
def write_utf8_bom(path, content): def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -29,7 +65,7 @@ def main():
# Дефолт консервативный: 2.17 читается всеми платформами. # Дефолт консервативный: 2.17 читается всеми платформами.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17', parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
choices=['2.17', '2.18', '2.19', '2.20', '2.21']) choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
args = parser.parse_args() args = ci_parse_args(parser)
name = args.Name name = args.Name
synonym = args.Synonym if args.Synonym else name synonym = args.Synonym if args.Synonym else name
@@ -54,6 +90,10 @@ def main():
co = [new_uuid() for _ in range(7)] co = [new_uuid() for _ in range(7)]
# --- Mobile functionalities --- # --- Mobile functionalities ---
# Версия формата как число — по ней ниже включаются вставки 2.21.
_fm = re.match(r'^(\d+)\.(\d+)$', args.FormatVersion)
is_221 = bool(_fm) and int(_fm.group(1)) * 100 + int(_fm.group(2)) >= 221
mobile_funcs = [ mobile_funcs = [
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"), ("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"), ("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
@@ -70,6 +110,10 @@ def main():
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"), ("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"), ("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
] ]
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.21 (8.5),
# последней в списке. На младших форматах платформа её не пишет.
if is_221:
mobile_funcs.append(("TextToSpeech", "false"))
mobile_xml = "" mobile_xml = ""
for func_name, func_use in mobile_funcs: for func_name, func_use in mobile_funcs:
@@ -78,10 +122,12 @@ def main():
# --- Synonym XML --- # --- Synonym XML ---
synonym_xml = "" synonym_xml = ""
if synonym: if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t" synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else "" # Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
version_xml = esc_xml(version) if version else "" # пишет <Vendor/>, а не <Vendor></Vendor>.
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
class_ids = [ class_ids = [
"9cd510cd-abfc-11d4-9434-004095e12fc7", "9cd510cd-abfc-11d4-9434-004095e12fc7",
@@ -93,6 +139,28 @@ def main():
"fb282519-d103-4dd3-bc12-cb271d631dfc", "fb282519-d103-4dd3-bc12-cb271d631dfc",
] ]
# Свойства и пространство имён формата 2.21 (платформа 8.5). Значения и ПОЗИЦИИ сняты
# с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники, выгруженные с 8.3.27 и
# с 8.5.1, различаются ровно этим. Порядок важен — вставки идут на своё место.
pal_ns = ""
f221_aux_forms = f221_window_variant = f221_open_variant = f221_captions = f221_migration = ""
if is_221:
pal_ns = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
f221_aux_forms = "\r\n" + "\r\n".join(
f"\t\t\t{t}" for t in (
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"))
f221_window_variant = ("\r\n\t\t\t<MainClientApplicationWindowInterfaceVariant>NavigationLeft"
"</MainClientApplicationWindowInterfaceVariant>"
"\r\n\t\t\t<ClientApplicationTheme>Auto</ClientApplicationTheme>")
f221_open_variant = ("\r\n\t\t\t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs"
"</ClientApplicationWindowsOpenVariant>")
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
f221_migration = ("\r\n\t\t\t<Version85InterfaceMigrationMode>DontUse"
"</Version85InterfaceMigrationMode>")
contained_objects = "" contained_objects = ""
for i in range(7): for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject> contained_objects += f"""\t\t\t<xr:ContainedObject>
@@ -101,12 +169,12 @@ def main():
\t\t\t</xr:ContainedObject>\n""" \t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?> cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"{pal_ns} xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
\t<Configuration uuid="{uuid_cfg}"> \t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo> \t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo> {contained_objects}\t\t</InternalInfo>
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name> \t\t\t<Name>{esc_xml_text(name)}</Name>
\t\t\t<Synonym>{synonym_xml}</Synonym> \t\t\t<Synonym>{synonym_xml}</Synonym>
\t\t\t<Comment/> \t\t\t<Comment/>
\t\t\t<NamePrefix/> \t\t\t<NamePrefix/>
@@ -117,8 +185,8 @@ def main():
\t\t\t</UsePurposes> \t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant> \t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles/> \t\t\t<DefaultRoles/>
\t\t\t<Vendor>{vendor_xml}</Vendor> \t\t\t{vendor_el}
\t\t\t<Version>{version_xml}</Version> \t\t\t{version_el}
\t\t\t<UpdateCatalogAddress/> \t\t\t<UpdateCatalogAddress/>
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents> \t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication> \t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
@@ -140,15 +208,15 @@ def main():
\t\t\t<DefaultDataHistoryChangeHistoryForm/> \t\t\t<DefaultDataHistoryChangeHistoryForm/>
\t\t\t<DefaultDataHistoryVersionDataForm/> \t\t\t<DefaultDataHistoryVersionDataForm/>
\t\t\t<DefaultDataHistoryVersionDifferencesForm/> \t\t\t<DefaultDataHistoryVersionDifferencesForm/>
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/> \t\t\t<DefaultCollaborationSystemUsersChoiceForm/>{f221_aux_forms}
\t\t\t<RequiredMobileApplicationPermissions/> \t\t\t<RequiredMobileApplicationPermissions/>
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml} \t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
\t\t\t</UsedMobileApplicationFunctionalities> \t\t\t</UsedMobileApplicationFunctionalities>
\t\t\t<StandaloneConfigurationRestrictionRoles/> \t\t\t<StandaloneConfigurationRestrictionRoles/>
\t\t\t<MobileApplicationURLs/> \t\t\t<MobileApplicationURLs/>
\t\t\t<AllowedIncomingShareRequestTypes/> \t\t\t<AllowedIncomingShareRequestTypes/>{f221_window_variant}
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode> \t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>{f221_open_variant}
\t\t\t<DefaultInterface/> \t\t\t<DefaultInterface/>{f221_captions}
\t\t\t<DefaultStyle/> \t\t\t<DefaultStyle/>
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage> \t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/> \t\t\t<BriefInformation/>
@@ -160,7 +228,7 @@ def main():
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode> \t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode> \t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode> \t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode> \t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>{f221_migration}
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode> \t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode> \t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
\t\t\t<DefaultConstantsForm/> \t\t\t<DefaultConstantsForm/>
@@ -173,7 +241,7 @@ def main():
# --- Languages/Русский.xml --- # --- Languages/Русский.xml ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?> lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"{pal_ns} xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
\t<Language uuid="{uuid_lang}"> \t<Language uuid="{uuid_lang}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>Русский</Name> \t\t\t<Name>Русский</Name>
@@ -222,11 +290,11 @@ def main():
os.makedirs(ext_dir, exist_ok=True) os.makedirs(ext_dir, exist_ok=True)
# --- Write files --- # --- Write files ---
write_utf8_bom(cfg_file, cfg_xml) write_xml_file(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml") lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml) write_xml_file(lang_file, lang_xml)
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml") cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
write_utf8_bom(cai_file, cai_xml) write_xml_file(cai_file, cai_xml)
print(f"[OK] Создана конфигурация: {name}") print(f"[OK] Создана конфигурация: {name}")
print(f" Каталог: {output_dir}") print(f" Каталог: {output_dir}")
@@ -1,4 +1,4 @@
# cf-validate v1.5 — Validate 1C configuration root structure # cf-validate v1.6 — Validate 1C configuration root structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -1,10 +1,32 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-validate v1.5 — Validate 1C configuration XML structure # cf-validate v1.6 — Validate 1C configuration XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages.""" """Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re import sys, os, argparse, re
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NS = { NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses', 'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core', 'v8': 'http://v8.1c.ru/8.1/data/core',
@@ -170,7 +192,7 @@ def main():
parser.add_argument('-Detailed', action='store_true') parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30) parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='') parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args() args = ci_parse_args(parser)
config_path = args.ConfigPath config_path = args.ConfigPath
max_errors = args.MaxErrors max_errors = args.MaxErrors
-1
View File
@@ -41,7 +41,6 @@ allowed-tools:
- `Enum.ВидыОплат` — перечисление - `Enum.ВидыОплат` — перечисление
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы) - `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов - `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
Поддерживаются все 44 типа объектов конфигурации.
### Заимствование форм ### Заимствование форм
@@ -1,4 +1,4 @@
# cfe-borrow v1.11 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.20 — Borrow objects from configuration into extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][string]$ExtensionPath, [Parameter(Mandatory)][string]$ExtensionPath,
@@ -125,7 +125,7 @@ $childTypeDirMap = @{
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices" "Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices" "XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
"HTTPService"="HTTPServices"; "WSReference"="WSReferences" "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"CommonAttribute"="CommonAttributes"; "Style"="Styles" "CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
} }
# --- 4b. Russian synonym → English type --- # --- 4b. Russian synonym → English type ---
@@ -153,7 +153,7 @@ $synonymMap = @{
$script:typeOrder = @( $script:typeOrder = @(
"Language","Subsystem","StyleItem","Style", "Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate", "CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", "FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference", "XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -368,6 +368,14 @@ function Expand-SelfClosingElement($container, $parentIndent) {
function Detect-FormatVersion([string]$dir) { function Detect-FormatVersion([string]$dir) {
$d = $dir $d = $dir
while ($d) { while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml" $cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) { if (Test-Path $cfgPath) {
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8) $cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
@@ -388,6 +396,20 @@ $script:formatVersion = Detect-FormatVersion $extDir
# --- 8. Namespaces declaration for object XML --- # --- 8. Namespaces declaration for object XML ---
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' $script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
# --- 9. Parse -Object into items --- # --- 9. Parse -Object into items ---
$items = @() $items = @()
foreach ($part in $Object.Split(";;")) { foreach ($part in $Object.Split(";;")) {
@@ -824,11 +846,22 @@ function Borrow-Form {
} }
} }
# Extract the <Form ...> opening tag from source text (preserves namespace declarations) # Открывающий тег <Form ...> берём из исходной формы — ради её объявлений пространств имён,
# но version подставляем СВОЮ: форма обязана нести версию расширения, иначе платформа
# отвергает импорт (форма 2.13 внутри расширения 2.17). Раньше тег копировался целиком,
# и версия источника молча побеждала.
$xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>' $xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>'
$formTag = "<Form version=`"${formVersion}`">" $formTag = "<Form version=`"${formVersion}`">"
if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] } if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] }
if ($srcFormContent -match '(<Form[^>]*>)') { $formTag = $Matches[1] } if ($srcFormContent -match '(<Form[^>]*>)') {
$srcTag = $Matches[1]
$srcNs = $srcTag -replace '^<Form\s*', '' -replace '\s*/?>$', '' -replace '\s*version="[^"]*"', ''
# 2.21 (8.5): пространство палитры. Место строгое — после lf, перед style.
if ((Get-FormatRank $formVersion) -ge 221 -and $srcNs -notmatch 'xmlns:pal=') {
$srcNs = $srcNs -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
$formTag = if ($srcNs) { "<Form $srcNs version=`"${formVersion}`">" } else { "<Form version=`"${formVersion}`">" }
}
# Build output Form.xml # Build output Form.xml
$formXmlSb = New-Object System.Text.StringBuilder $formXmlSb = New-Object System.Text.StringBuilder
@@ -917,7 +950,15 @@ function Borrow-Form {
New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null
} }
$formXmlFile = Join-Path $formXmlDir "Form.xml" $formXmlFile = Join-Path $formXmlDir "Form.xml"
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc) # Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
# Здесь источник не XmlWriter, а OuterXml исходного документа — спацовывает так же.
$formXmlText = $formXmlSb.ToString()
$formXmlText = [regex]::Replace($formXmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Файл создаём мы — канон выгрузки: CRLF в разделителях строк.
$formXmlText = ($formXmlText -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($formXmlFile, $formXmlText, $enc)
Info " Created: $formXmlFile" Info " Created: $formXmlFile"
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
@@ -1023,8 +1064,16 @@ function Register-FormInObject {
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2) $text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) } if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"') $text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text2 = [regex]::Replace($text2, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true) $utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text2 = ($text2 -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2) [System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
Info " Registered form in: $objFile" Info " Registered form in: $objFile"
} }
@@ -1413,7 +1462,17 @@ function Merge-AttributesIntoObject {
# Insert attributes before </ChildObjects> # Insert attributes before </ChildObjects>
$text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>" $text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>"
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
# Стоит ПОСЛЕ вставки реквизитов, чтобы накрыть и их.
$text3 = [regex]::Replace($text3, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true) $utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text3 = ($text3 -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3) [System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
Info " Merged $added attribute(s) into: $objFile" Info " Merged $added attribute(s) into: $objFile"
} }
@@ -1877,8 +1936,16 @@ $memStream.Close()
$text = [System.Text.Encoding]::UTF8.GetString($bytes) $text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') $text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $utf8Bom = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $extResolvedPath) -and ([System.IO.File]::ReadAllText($extResolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom) [System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
Info "Saved: $extResolvedPath" Info "Saved: $extResolvedPath"
+111 -29
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-borrow v1.11 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.20 — Borrow objects from configuration into extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -9,6 +9,28 @@ import sys
import uuid import uuid
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
MD_NS = "http://v8.1c.ru/8.3/MDClasses" MD_NS = "http://v8.1c.ru/8.3/MDClasses"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable" XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
@@ -80,6 +102,7 @@ CHILD_TYPE_DIR_MAP = {
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "XDTOPackage": "XDTOPackages", "WebService": "WebServices",
"HTTPService": "HTTPServices", "WSReference": "WSReferences", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
"CommonAttribute": "CommonAttributes", "Style": "Styles", "CommonAttribute": "CommonAttributes", "Style": "Styles",
"Bot": "Bots", "Language": "Languages",
} }
SYNONYM_MAP = { SYNONYM_MAP = {
@@ -123,7 +146,7 @@ SYNONYM_MAP = {
TYPE_ORDER = [ TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style", "Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate", "CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan", "FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference", "XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption", "EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup", "FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -309,6 +332,16 @@ XMLNS_DECL = (
def detect_format_version(d): def detect_format_version(d):
while d: while d:
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
ext_path = d + ".xml"
if os.path.isfile(ext_path):
with open(ext_path, "r", encoding="utf-8-sig") as f:
ext_head = f.read(2000)
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
if m:
return m.group(1)
cfg_path = os.path.join(d, "Configuration.xml") cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path): if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f: with open(cfg_path, "r", encoding="utf-8-sig") as f:
@@ -323,6 +356,23 @@ def detect_format_version(d):
return "2.17" return "2.17"
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def apply_pal_ns(format_version):
"""2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
дописать в конец нельзя."""
global XMLNS_DECL
if format_rank(format_version) >= 221:
XMLNS_DECL = XMLNS_DECL.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
def get_child_indent(container): def get_child_indent(container):
if container.text and "\n" in container.text: if container.text and "\n" in container.text:
after_nl = container.text.rsplit("\n", 1)[-1] after_nl = container.text.rsplit("\n", 1)[-1]
@@ -389,21 +439,22 @@ def _detect_xml_style(path):
def _finalize_xml_bytes(xml_bytes, style): def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None).""" """Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
enc_decl = style["enc"] if style else "utf-8" выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace( xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>", b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>') b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах) # Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"") xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n")) .replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть) # Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else True want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n") xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl: if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение) # EOL — как в оригинале (новый файл → CRLF, канон #57)
if style and style["crlf"]: if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n") xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes return xml_bytes
@@ -418,9 +469,24 @@ def save_xml_bom(tree, path):
f.write(xml_bytes) f.write(xml_bytes)
def save_text_bom(path, text): def write_utf8_bom(path, content):
with open(path, "w", encoding="utf-8-sig") as fh: # newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
fh.write(text) # и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
Только для файлов, которые СОЗДАЁМ: правка существующего наследует его стиль.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def new_guid(): def new_guid():
@@ -435,7 +501,7 @@ def main():
parser.add_argument("-ConfigPath", required=True) parser.add_argument("-ConfigPath", required=True)
parser.add_argument("-Object", required=True) parser.add_argument("-Object", required=True)
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None) parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
args = parser.parse_args() args = ci_parse_args(parser)
# --- 1. Resolve paths --- # --- 1. Resolve paths ---
ext_path = args.ExtensionPath ext_path = args.ExtensionPath
@@ -471,6 +537,7 @@ def main():
cfg_dir = os.path.dirname(cfg_resolved) cfg_dir = os.path.dirname(cfg_resolved)
format_version = detect_format_version(ext_dir) format_version = detect_format_version(ext_dir)
apply_pal_ns(format_version)
# --- 2. Load extension Configuration.xml --- # --- 2. Load extension Configuration.xml ---
xml_parser = etree.XMLParser(remove_blank_text=False) xml_parser = etree.XMLParser(remove_blank_text=False)
@@ -997,7 +1064,9 @@ def main():
warn(f"Cannot merge attributes: {obj_file} not found") warn(f"Cannot merge attributes: {obj_file} not found")
return return
with open(obj_file, "r", encoding="utf-8-sig") as fh: # newline="" => без трансляции: иначе CRLF молча схлопнется в LF при чтении
# и файл будет переписан в LF.
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
obj_content = fh.read() obj_content = fh.read()
# Collect existing attribute names for dedup (text-based) # Collect existing attribute names for dedup (text-based)
@@ -1019,7 +1088,7 @@ def main():
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>{all_attr_xml}\r\n\t\t</ChildObjects>", obj_content) obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>{all_attr_xml}\r\n\t\t</ChildObjects>", obj_content)
else: else:
obj_content = obj_content.replace("</ChildObjects>", f"{all_attr_xml}\r\n\t\t</ChildObjects>") obj_content = obj_content.replace("</ChildObjects>", f"{all_attr_xml}\r\n\t\t</ChildObjects>")
save_text_bom(obj_file, obj_content) write_utf8_bom(obj_file, obj_content)
info(f" Merged {added} attribute(s) into: {obj_file}") info(f" Merged {added} attribute(s) into: {obj_file}")
# --- 11h. Borrow main attribute orchestrator --- # --- 11h. Borrow main attribute orchestrator ---
@@ -1056,7 +1125,9 @@ def main():
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml") obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
# Read existing object XML (needed for dedup + enrichment) # Read existing object XML (needed for dedup + enrichment)
with open(obj_file, "r", encoding="utf-8-sig") as fh: # newline="" => без трансляции: иначе CRLF молча схлопнется в LF при чтении
# и файл будет переписан в LF.
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
obj_content = fh.read() obj_content = fh.read()
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow) # Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
@@ -1104,7 +1175,7 @@ def main():
f"<ChildObjects>{existing_inner}\r\n{adopted_content}\r\n\t\t</ChildObjects>" f"<ChildObjects>{existing_inner}\r\n{adopted_content}\r\n\t\t</ChildObjects>"
) )
save_text_bom(obj_file, obj_content) write_utf8_bom(obj_file, obj_content)
info(f" Enriched object: {obj_file}") info(f" Enriched object: {obj_file}")
# Step 4: Collect all reference types and borrow as shells # Step 4: Collect all reference types and borrow as shells
@@ -1133,7 +1204,7 @@ def main():
target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]]) target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]])
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{rt['ObjName']}.xml") target_file = os.path.join(target_dir, f"{rt['ObjName']}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
add_to_child_objects(rt["TypeName"], rt["ObjName"]) add_to_child_objects(rt["TypeName"], rt["ObjName"])
borrowed_files.append(target_file) borrowed_files.append(target_file)
info(f" Auto-borrowed: {rt['TypeName']}.{rt['ObjName']}") info(f" Auto-borrowed: {rt['TypeName']}.{rt['ObjName']}")
@@ -1198,7 +1269,7 @@ def main():
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name]) t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
os.makedirs(t_target_dir, exist_ok=True) os.makedirs(t_target_dir, exist_ok=True)
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml") t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
save_text_bom(t_target_file, t_borrowed_xml) write_xml_file(t_target_file, t_borrowed_xml)
add_to_child_objects(target_type_name, target_obj_name) add_to_child_objects(target_type_name, target_obj_name)
borrowed_files.append(t_target_file) borrowed_files.append(t_target_file)
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}") info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
@@ -1222,7 +1293,7 @@ def main():
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]]) s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True) os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml") s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml) write_xml_file(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"]) add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file) borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}") info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
@@ -1279,7 +1350,7 @@ def main():
os.makedirs(form_meta_dir, exist_ok=True) os.makedirs(form_meta_dir, exist_ok=True)
form_meta_file = os.path.join(form_meta_dir, f"{form_name}.xml") form_meta_file = os.path.join(form_meta_dir, f"{form_name}.xml")
save_text_bom(form_meta_file, "\n".join(form_meta_lines)) write_xml_file(form_meta_file, "\n".join(form_meta_lines))
info(f" Created: {form_meta_file}") info(f" Created: {form_meta_file}")
# 5. Generate Form.xml with BaseForm # 5. Generate Form.xml with BaseForm
@@ -1365,7 +1436,7 @@ def main():
target_dir = os.path.join(ext_dir, "CommonPictures") target_dir = os.path.join(ext_dir, "CommonPictures")
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{pic_name}.xml") target_file = os.path.join(target_dir, f"{pic_name}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
add_to_child_objects("CommonPicture", pic_name) add_to_child_objects("CommonPicture", pic_name)
auto_borrowed_pics.append(pic_name) auto_borrowed_pics.append(pic_name)
borrowed_files.append(target_file) borrowed_files.append(target_file)
@@ -1414,7 +1485,7 @@ def main():
target_dir = os.path.join(ext_dir, "StyleItems") target_dir = os.path.join(ext_dir, "StyleItems")
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{style_name}.xml") target_file = os.path.join(target_dir, f"{style_name}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
add_to_child_objects("StyleItem", style_name) add_to_child_objects("StyleItem", style_name)
borrowed_files.append(target_file) borrowed_files.append(target_file)
info(f" Auto-borrowed: StyleItem.{style_name}") info(f" Auto-borrowed: StyleItem.{style_name}")
@@ -1478,14 +1549,17 @@ def main():
target_dir = os.path.join(ext_dir, "Enums") target_dir = os.path.join(ext_dir, "Enums")
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{enum_name}.xml") target_file = os.path.join(target_dir, f"{enum_name}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
add_to_child_objects("Enum", enum_name) add_to_child_objects("Enum", enum_name)
borrowed_files.append(target_file) borrowed_files.append(target_file)
info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))") info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))")
else: else:
warn(f" Enum.{enum_name} not found in source config") warn(f" Enum.{enum_name} not found in source config")
# Extract the <Form ...> opening tag from source text # Открывающий тег <Form ...> берём из исходной формы — ради её объявлений пространств
# имён, но version подставляем СВОЮ: форма обязана нести версию расширения, иначе
# платформа отвергает импорт (форма 2.13 внутри расширения 2.17). Раньше тег
# копировался целиком, и версия источника молча побеждала.
xml_decl = '<?xml version="1.0" encoding="UTF-8"?>' xml_decl = '<?xml version="1.0" encoding="UTF-8"?>'
form_tag = f'<Form version="{form_version}">' form_tag = f'<Form version="{form_version}">'
m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content) m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content)
@@ -1493,7 +1567,15 @@ def main():
xml_decl = m_decl.group(1) xml_decl = m_decl.group(1)
m_tag = re.search(r'(<Form[^>]*>)', src_form_content) m_tag = re.search(r'(<Form[^>]*>)', src_form_content)
if m_tag: if m_tag:
form_tag = m_tag.group(1) src_ns = re.sub(r'^<Form\s*', '', m_tag.group(1))
src_ns = re.sub(r'\s*/?>$', '', src_ns)
src_ns = re.sub(r'\s*version="[^"]*"', '', src_ns)
# 2.21 (8.5): пространство палитры. Место строгое — после lf, перед style.
if format_rank(form_version) >= 221 and 'xmlns:pal=' not in src_ns:
src_ns = src_ns.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
form_tag = f'<Form {src_ns} version="{form_version}">' if src_ns else f'<Form version="{form_version}">'
# Build output # Build output
parts = [] parts = []
@@ -1572,7 +1654,7 @@ def main():
form_xml_dir = os.path.join(form_meta_dir, form_name, "Ext") form_xml_dir = os.path.join(form_meta_dir, form_name, "Ext")
os.makedirs(form_xml_dir, exist_ok=True) os.makedirs(form_xml_dir, exist_ok=True)
form_xml_file = os.path.join(form_xml_dir, "Form.xml") form_xml_file = os.path.join(form_xml_dir, "Form.xml")
save_text_bom(form_xml_file, "".join(parts)) write_xml_file(form_xml_file, "".join(parts))
info(f" Created: {form_xml_file}") info(f" Created: {form_xml_file}")
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
@@ -1583,7 +1665,7 @@ def main():
if os.path.isfile(module_bsl_file): if os.path.isfile(module_bsl_file):
info(" Preserved existing Module.bsl") info(" Preserved existing Module.bsl")
else: else:
save_text_bom(module_bsl_file, "") write_utf8_bom(module_bsl_file, "")
info(f" Created: {module_bsl_file}") info(f" Created: {module_bsl_file}")
# 7. Register form in parent object ChildObjects # 7. Register form in parent object ChildObjects
@@ -1656,7 +1738,7 @@ def main():
target_dir = os.path.join(ext_dir, dir_name) target_dir = os.path.join(ext_dir, dir_name)
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{obj_name}.xml") target_file = os.path.join(target_dir, f"{obj_name}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
info(f" Created: {target_file}") info(f" Created: {target_file}")
add_to_child_objects(type_name, obj_name) add_to_child_objects(type_name, obj_name)
@@ -1683,7 +1765,7 @@ def main():
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{obj_name}.xml") target_file = os.path.join(target_dir, f"{obj_name}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
info(f" Created: {target_file}") info(f" Created: {target_file}")
add_to_child_objects(type_name, obj_name) add_to_child_objects(type_name, obj_name)
+4 -2
View File
@@ -1,4 +1,4 @@
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE) # cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -49,7 +49,9 @@ $childTypeDirMap = @{
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices" "Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
"CommonAttribute"="CommonAttributes" "CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"Bot"="Bots"
} }
# --- Parse extension Configuration.xml --- # --- Parse extension Configuration.xml ---
+30 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE) # cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -8,6 +8,28 @@ import re
import sys import sys
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Namespace maps --- # --- Namespace maps ---
MD_NSMAP = { MD_NSMAP = {
@@ -60,6 +82,12 @@ CHILD_TYPE_DIR_MAP = {
"Sequence": "Sequences", "Sequence": "Sequences",
"IntegrationService": "IntegrationServices", "IntegrationService": "IntegrationServices",
"CommonAttribute": "CommonAttributes", "CommonAttribute": "CommonAttributes",
"Style": "Styles",
"XDTOPackage": "XDTOPackages",
"WebService": "WebServices",
"HTTPService": "HTTPServices",
"WSReference": "WSReferences",
"Bot": "Bots",
} }
@@ -468,7 +496,7 @@ def main():
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root") parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root") parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check") parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
args = parser.parse_args() args = ci_parse_args(parser)
extension_path = args.ExtensionPath extension_path = args.ExtensionPath
config_path = args.ConfigPath config_path = args.ConfigPath
+58 -18
View File
@@ -1,4 +1,4 @@
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE) # cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -16,6 +16,12 @@ param(
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
function Esc-XmlText {
param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Default NamePrefix --- # --- Default NamePrefix ---
@@ -121,20 +127,23 @@ $co7 = [guid]::NewGuid().ToString()
# --- Synonym XML --- # --- Synonym XML ---
$synonymXml = "" $synonymXml = ""
if ($Synonym) { if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t" $synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
} }
# --- Optional properties --- # --- Optional properties ---
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } # Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } # пишет <Vendor/>, а не <Vendor></Vendor>.
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
# --- Role name --- # --- Role name ---
$roleName = "${NamePrefix}ОсновнаяРоль" $roleName = "${NamePrefix}ОсновнаяРоль"
# --- DefaultRoles XML --- # --- DefaultRoles XML ---
$defaultRolesXml = "" # Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
$defaultRolesEl = "<DefaultRoles/>"
if (-not $NoRole) { if (-not $NoRole) {
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t" $defaultRolesEl = "<DefaultRoles>`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t</DefaultRoles>"
} }
# --- ChildObjects --- # --- ChildObjects ---
@@ -144,10 +153,32 @@ if (-not $NoRole) {
} }
$childObjectsXml += "`r`n`t`t" $childObjectsXml += "`r`n`t`t"
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
$f221Captions = ""
if ((Get-FormatRank $formatVersion) -ge 221) {
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
$f221Captions = "`r`n`t`t`t<Caption/>`r`n`t`t`t<ShortCaption/>"
}
# --- Configuration.xml --- # --- Configuration.xml ---
$cfgXml = @" $cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion"> <MetaDataObject $xmlnsDecl version="$formatVersion">
<Configuration uuid="$uuidCfg"> <Configuration uuid="$uuidCfg">
<InternalInfo> <InternalInfo>
<xr:ContainedObject> <xr:ContainedObject>
@@ -181,21 +212,21 @@ $cfgXml = @"
</InternalInfo> </InternalInfo>
<Properties> <Properties>
<ObjectBelonging>Adopted</ObjectBelonging> <ObjectBelonging>Adopted</ObjectBelonging>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name> <Name>$(Esc-XmlText ($Name))</Name>
<Synonym>$synonymXml</Synonym> <Synonym>$synonymXml</Synonym>
<Comment/> <Comment/>
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose> <ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs> <KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix> <NamePrefix>$(Esc-XmlText ($NamePrefix))</NamePrefix>
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode> <ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
<DefaultRunMode>ManagedApplication</DefaultRunMode> <DefaultRunMode>ManagedApplication</DefaultRunMode>
<UsePurposes> <UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> <v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles>$defaultRolesXml</DefaultRoles> $defaultRolesEl
<Vendor>$vendorXml</Vendor> $vendorEl
<Version>$versionXml</Version> $versionEl$f221Captions
<DefaultLanguage>Language.Русский</DefaultLanguage> <DefaultLanguage>Language.Русский</DefaultLanguage>
<BriefInformation/> <BriefInformation/>
<DetailedInformation/> <DetailedInformation/>
@@ -212,7 +243,7 @@ $cfgXml = @"
# --- Languages/Русский.xml (adopted format) --- # --- Languages/Русский.xml (adopted format) ---
$langXml = @" $langXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion"> <MetaDataObject $xmlnsDecl version="$formatVersion">
<Language uuid="$uuidLang"> <Language uuid="$uuidLang">
<InternalInfo/> <InternalInfo/>
<Properties> <Properties>
@@ -229,10 +260,10 @@ $langXml = @"
# --- Role XML --- # --- Role XML ---
$roleXml = @" $roleXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion"> <MetaDataObject $xmlnsDecl version="$formatVersion">
<Role uuid="$uuidRole"> <Role uuid="$uuidRole">
<Properties> <Properties>
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name> <Name>$(Esc-XmlText ($roleName))</Name>
<Synonym/> <Synonym/>
<Comment/> <Comment/>
</Properties> </Properties>
@@ -252,9 +283,18 @@ if (-not (Test-Path $langDir)) {
# --- Write files with UTF-8 BOM --- # --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) # XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $cfgFile $cfgXml $enc
$langFile = Join-Path $langDir "Русский.xml" $langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc) Write-XmlFile $langFile $langXml $enc
# --- Role --- # --- Role ---
if (-not $NoRole) { if (-not $NoRole) {
@@ -263,7 +303,7 @@ if (-not $NoRole) {
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
} }
$roleFile = Join-Path $roleDir "$roleName.xml" $roleFile = Join-Path $roleDir "$roleName.xml"
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc) Write-XmlFile $roleFile $roleXml $enc
} }
# --- Output --- # --- Output ---
+103 -22
View File
@@ -1,20 +1,62 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE) # cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension.""" """Generates minimal XML source files for a 1C configuration extension."""
import sys, os, argparse, uuid import sys, os, re, argparse, uuid
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
def esc_xml(s): # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;') # регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def new_uuid(): def new_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
def write_utf8_bom(path, content): def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -29,7 +71,7 @@ def main():
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24') parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None) parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
parser.add_argument('-NoRole', dest='NoRole', action='store_true') parser.add_argument('-NoRole', dest='NoRole', action='store_true')
args = parser.parse_args() args = ci_parse_args(parser)
name = args.Name name = args.Name
synonym = args.Synonym if args.Synonym else name synonym = args.Synonym if args.Synonym else name
@@ -126,18 +168,23 @@ def main():
# --- Synonym XML --- # --- Synonym XML ---
synonym_xml = "" synonym_xml = ""
if synonym: if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t" synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else "" # Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
version_xml = esc_xml(version) if version else "" # пишет <Vendor/>, а не <Vendor></Vendor>.
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
# --- Role name --- # --- Role name ---
role_name = f"{name_prefix}ОсновнаяРоль" role_name = f"{name_prefix}ОсновнаяРоль"
# --- DefaultRoles XML --- # --- DefaultRoles XML ---
default_roles_xml = "" # Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
default_roles_el = "<DefaultRoles/>"
if not args.NoRole: if not args.NoRole:
default_roles_xml = f'\r\n\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>\r\n\t\t\t' default_roles_el = ('<DefaultRoles>\r\n\t\t\t\t'
f'<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>'
'\r\n\t\t\t</DefaultRoles>')
# --- ChildObjects --- # --- ChildObjects ---
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>" child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
@@ -156,6 +203,40 @@ def main():
] ]
contained_objects = "" contained_objects = ""
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
xmlns_decl = (
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
f221_captions = ""
if format_rank(format_version) >= 221:
xmlns_decl = xmlns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
for i in range(7): for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject> contained_objects += f"""\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId> \t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
@@ -163,27 +244,27 @@ def main():
\t\t\t</xr:ContainedObject>\n""" \t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?> cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}"> <MetaDataObject {xmlns_decl} version="{format_version}">
\t<Configuration uuid="{uuid_cfg}"> \t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo> \t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo> {contained_objects}\t\t</InternalInfo>
\t\t<Properties> \t\t<Properties>
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging> \t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
\t\t\t<Name>{esc_xml(name)}</Name> \t\t\t<Name>{esc_xml_text(name)}</Name>
\t\t\t<Synonym>{synonym_xml}</Synonym> \t\t\t<Synonym>{synonym_xml}</Synonym>
\t\t\t<Comment/> \t\t\t<Comment/>
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose> \t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs> \t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
\t\t\t<NamePrefix>{esc_xml(name_prefix)}</NamePrefix> \t\t\t<NamePrefix>{esc_xml_text(name_prefix)}</NamePrefix>
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode> \t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode> \t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
\t\t\t<UsePurposes> \t\t\t<UsePurposes>
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> \t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
\t\t\t</UsePurposes> \t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant> \t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles>{default_roles_xml}</DefaultRoles> \t\t\t{default_roles_el}
\t\t\t<Vendor>{vendor_xml}</Vendor> \t\t\t{vendor_el}
\t\t\t<Version>{version_xml}</Version> \t\t\t{version_el}{f221_captions}
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage> \t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/> \t\t\t<BriefInformation/>
\t\t\t<DetailedInformation/> \t\t\t<DetailedInformation/>
@@ -198,7 +279,7 @@ def main():
# --- Languages/Русский.xml (adopted format) --- # --- Languages/Русский.xml (adopted format) ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?> lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}"> <MetaDataObject {xmlns_decl} version="{format_version}">
\t<Language uuid="{uuid_lang}"> \t<Language uuid="{uuid_lang}">
\t\t<InternalInfo/> \t\t<InternalInfo/>
\t\t<Properties> \t\t<Properties>
@@ -213,10 +294,10 @@ def main():
# --- Role XML --- # --- Role XML ---
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?> role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}"> <MetaDataObject {xmlns_decl} version="{format_version}">
\t<Role uuid="{uuid_role}"> \t<Role uuid="{uuid_role}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>{esc_xml(role_name)}</Name> \t\t\t<Name>{esc_xml_text(role_name)}</Name>
\t\t\t<Synonym/> \t\t\t<Synonym/>
\t\t\t<Comment/> \t\t\t<Comment/>
\t\t</Properties> \t\t</Properties>
@@ -229,9 +310,9 @@ def main():
os.makedirs(lang_dir, exist_ok=True) os.makedirs(lang_dir, exist_ok=True)
# --- Write files --- # --- Write files ---
write_utf8_bom(cfg_file, cfg_xml) write_xml_file(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml") lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml) write_xml_file(lang_file, lang_xml)
# --- Role --- # --- Role ---
role_file = None role_file = None
@@ -239,7 +320,7 @@ def main():
role_dir = os.path.join(output_dir, "Roles") role_dir = os.path.join(output_dir, "Roles")
os.makedirs(role_dir, exist_ok=True) os.makedirs(role_dir, exist_ok=True)
role_file = os.path.join(role_dir, f"{role_name}.xml") role_file = os.path.join(role_dir, f"{role_name}.xml")
write_utf8_bom(role_file, role_xml) write_xml_file(role_file, role_xml)
# --- Output --- # --- Output ---
print(f"[OK] Создано расширение: {name}") print(f"[OK] Создано расширение: {name}")
@@ -1,4 +1,4 @@
# cfe-patch-method v2.5 — Source-aware method interceptor for 1C extension (CFE) # cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-patch-method v2.5 — Source-aware method interceptor for 1C extension (CFE) # cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -10,6 +10,28 @@ import sys
import tempfile import tempfile
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
TYPE_DIR_MAP = { TYPE_DIR_MAP = {
"Catalog": "Catalogs", "Document": "Documents", "Enum": "Enums", "Catalog": "Catalogs", "Document": "Documents", "Enum": "Enums",
"CommonModule": "CommonModules", "Report": "Reports", "DataProcessor": "DataProcessors", "CommonModule": "CommonModules", "Report": "Reports", "DataProcessor": "DataProcessors",
@@ -19,6 +41,24 @@ TYPE_DIR_MAP = {
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "BusinessProcess": "BusinessProcesses", "Task": "Tasks",
"InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters", "InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
"AccountingRegister": "AccountingRegisters", "CalculationRegister": "CalculationRegisters", "AccountingRegister": "AccountingRegisters", "CalculationRegister": "CalculationRegisters",
# Прощающий ввод: имя каталога принимается наравне с именем типа (Catalogs.X ≡ Catalog.X) —
# PS1-порт так умел с самого начала, PY отставал.
"Catalogs": "Catalogs",
"Documents": "Documents",
"Enums": "Enums",
"CommonModules": "CommonModules",
"Reports": "Reports",
"DataProcessors": "DataProcessors",
"ExchangePlans": "ExchangePlans",
"ChartsOfAccounts": "ChartsOfAccounts",
"ChartsOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartsOfCalculationTypes": "ChartsOfCalculationTypes",
"BusinessProcesses": "BusinessProcesses",
"Tasks": "Tasks",
"InformationRegisters": "InformationRegisters",
"AccumulationRegisters": "AccumulationRegisters",
"AccountingRegisters": "AccountingRegisters",
"CalculationRegisters": "CalculationRegisters",
} }
# accept plural forms too # accept plural forms too
for _v in list(TYPE_DIR_MAP.values()): for _v in list(TYPE_DIR_MAP.values()):
@@ -539,7 +579,7 @@ def main():
choices=["", "Before", "After", "Instead", "ModificationAndControl"]) choices=["", "Before", "After", "Instead", "ModificationAndControl"])
parser.add_argument("-Check", action="store_true") parser.add_argument("-Check", action="store_true")
parser.add_argument("-Actualize", action="store_true") parser.add_argument("-Actualize", action="store_true")
args = parser.parse_args() args = ci_parse_args(parser)
extension_path = args.ExtensionPath extension_path = args.ExtensionPath
config_path = args.ConfigPath config_path = args.ConfigPath
@@ -1,4 +1,4 @@
# cfe-validate v1.5 — Validate 1C configuration extension structure (CFE) # cfe-validate v1.7 — Validate 1C configuration extension structure (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -108,7 +108,7 @@ $validClassIds = @(
$childObjectTypes = @( $childObjectTypes = @(
"Language","Subsystem","StyleItem","Style", "Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate", "CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", "FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference", "XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -122,7 +122,7 @@ $childObjectTypes = @(
# Type -> directory mapping # Type -> directory mapping
$childTypeDirMap = @{ $childTypeDirMap = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles" "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules" "CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages" "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
@@ -1,10 +1,32 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-validate v1.5 — Validate 1C configuration extension XML structure (CFE) # cfe-validate v1.7 — Validate 1C configuration extension XML structure (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects.""" """Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re import sys, os, argparse, re
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NS = { NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses', 'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core', 'v8': 'http://v8.1c.ru/8.1/data/core',
@@ -37,7 +59,7 @@ VALID_CLASS_IDS = [
CHILD_OBJECT_TYPES = [ CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style', 'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate', 'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan', 'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference', 'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption', 'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup', 'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
@@ -54,6 +76,7 @@ CHILD_TYPE_DIR_MAP = {
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles', 'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles', 'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules', 'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
'Bot': 'Bots',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages', 'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences', 'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs', 'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
@@ -154,7 +177,7 @@ def main():
parser.add_argument('-Detailed', action='store_true') parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30) parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='') parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args() args = ci_parse_args(parser)
extension_path = args.ExtensionPath extension_path = args.ExtensionPath
max_errors = args.MaxErrors max_errors = args.MaxErrors
@@ -1,4 +1,4 @@
# db-create v1.10 — Create 1C information base # db-create v1.11 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
+24 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-create v1.10 — Create 1C information base # db-create v1.11 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -355,7 +377,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
@@ -1,4 +1,4 @@
# db-dump-cf v1.12 — Dump 1C configuration to CF file # db-dump-cf v1.13 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-cf v1.12 — Dump 1C configuration to CF file # db-dump-cf v1.13 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -375,7 +397,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
@@ -1,4 +1,4 @@
# db-dump-dt v1.11 — Dump 1C information base to DT file # db-dump-dt v1.12 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-dt v1.11 — Dump 1C information base to DT file # db-dump-dt v1.12 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -373,7 +395,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
@@ -1,4 +1,4 @@
# db-dump-xml v1.14 — Dump 1C configuration to XML files # db-dump-xml v1.15 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-xml v1.14 — Dump 1C configuration to XML files # db-dump-xml v1.15 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -388,7 +410,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
@@ -1,4 +1,4 @@
# db-load-cf v1.13 — Load 1C configuration from CF file # db-load-cf v1.14 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-cf v1.13 — Load 1C configuration from CF file # db-load-cf v1.14 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -393,7 +415,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
@@ -1,4 +1,4 @@
# db-load-dt v1.12 — Load 1C information base from DT file # db-load-dt v1.13 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-dt v1.12 — Load 1C information base from DT file # db-load-dt v1.13 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -393,7 +415,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
@@ -1,4 +1,4 @@
# db-load-git v1.18 — Load Git changes into 1C database # db-load-git v1.19 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-git v1.18 — Load Git changes into 1C database # db-load-git v1.19 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -430,7 +452,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
@@ -1,4 +1,4 @@
# db-load-xml v1.19 — Load 1C configuration from XML files # db-load-xml v1.20 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-xml v1.19 — Load 1C configuration from XML files # db-load-xml v1.20 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -413,7 +435,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
+1 -1
View File
@@ -1,4 +1,4 @@
# db-run v1.7 — Launch 1C:Enterprise # db-run v1.8 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
+24 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-run v1.7 — Launch 1C:Enterprise # db-run v1.8 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -11,6 +11,28 @@ import subprocess
import sys import sys
import time import time
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -281,7 +303,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
@@ -1,4 +1,4 @@
# db-update v1.13 — Update 1C database configuration # db-update v1.14 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
+24 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-update v1.13 — Update 1C database configuration # db-update v1.14 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -395,7 +417,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
@@ -1,4 +1,4 @@
# epf-build v1.12 — Build external data processor or report (EPF/ERF) from XML sources # epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
+24 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-build v1.12 — Build external data processor or report (EPF/ERF) from XML sources # epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -374,7 +396,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
+1 -1
View File
@@ -1,4 +1,4 @@
# epf-dump v1.11 — Dump external data processor or report (EPF/ERF) to XML sources # epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
+24 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-dump v1.11 — Dump external data processor or report (EPF/ERF) to XML sources # epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,28 @@ import subprocess
import sys import sys
import tempfile import tempfile
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
@@ -380,7 +402,7 @@ def main():
help="Extra ibcmd arguments in --key=value form") help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings} known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv) args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
+8 -7
View File
@@ -18,19 +18,20 @@ allowed-tools:
## Usage ## Usage
``` ```
/epf-init <Name> [Synonym] [SrcDir] /epf-init <Name> [Synonym] [SrcDir] [FormatVersion]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|-------------------------------------| |---------------|:------------:|--------------|------------------------------------------------|
| Name | да | — | Имя обработки (латиница/кириллица) | | Name | да | — | Имя обработки (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) | | Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD | | SrcDir | нет | `src` | Каталог исходников относительно CWD |
| FormatVersion | нет | `2.17` | Версия формата: 2.20 — платформа 8.3.27, 2.21 — 8.5. Дефолт открывается любой платформой |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<2.17|2.18|2.19|2.20|2.21>"]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
+40 -6
View File
@@ -1,4 +1,4 @@
# epf-init v1.1 — Init 1C external data processor scaffold # epf-init v1.7 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -6,10 +6,22 @@ param(
[string]$Synonym = $Name, [string]$Synonym = $Name,
[string]$SrcDir = "src" [string]$SrcDir = "src",
# Версия формата выгрузки. Своей конфигурации у автономной обработки нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри обработки
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
function Esc-XmlText {
param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8 [Console]::InputEncoding = [System.Text.Encoding]::UTF8
@@ -18,9 +30,17 @@ $uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString() $uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString() $uuid4 = [guid]::NewGuid().ToString()
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
# платформа держит объявления по алфавиту, дописать в конец нельзя.
if (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) {
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
$xml = @" $xml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject $xmlnsDecl version="$FormatVersion">
<ExternalDataProcessor uuid="$uuid1"> <ExternalDataProcessor uuid="$uuid1">
<InternalInfo> <InternalInfo>
<xr:ContainedObject> <xr:ContainedObject>
@@ -33,11 +53,11 @@ $xml = @"
</xr:GeneratedType> </xr:GeneratedType>
</InternalInfo> </InternalInfo>
<Properties> <Properties>
<Name>$Name</Name> <Name>$(Esc-XmlText $Name)</Name>
<Synonym> <Synonym>
<v8:item> <v8:item>
<v8:lang>ru</v8:lang> <v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content> <v8:content>$(Esc-XmlText $Synonym)</v8:content>
</v8:item> </v8:item>
</Synonym> </Synonym>
<Comment/> <Comment/>
@@ -64,7 +84,16 @@ $extDir = Join-Path $processorDir "Ext"
New-Item -ItemType Directory -Path $extDir -Force | Out-Null New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) # XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc
# --- Модуль объекта --- # --- Модуль объекта ---
@@ -83,6 +112,11 @@ $moduleBsl = @"
"@ "@
$modulePath = Join-Path $extDir "ObjectModule.bsl" $modulePath = Join-Path $extDir "ObjectModule.bsl"
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
# самого скрипта, а он в репозитории хранится с LF.
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создана обработка: $rootFile" Write-Host "[OK] Создана обработка: $rootFile"
+88 -10
View File
@@ -1,19 +1,61 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-init v1.1 — Init 1C external data processor scaffold # epf-init v1.7 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external data processor.""" """Generates minimal XML source files for a 1C external data processor."""
import sys, os, argparse, uuid import sys, os, re, argparse, uuid
def esc_xml(s): # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;') # регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def new_uuid(): def new_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
def write_utf8_bom(path, content): def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -21,7 +63,12 @@ def main():
parser.add_argument('-Name', dest='Name', required=True) parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None) parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src') parser.add_argument('-SrcDir', dest='SrcDir', default='src')
args = parser.parse_args() # Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
args = ci_parse_args(parser)
name = args.Name name = args.Name
synonym = args.Synonym if args.Synonym else name synonym = args.Synonym if args.Synonym else name
@@ -32,8 +79,36 @@ def main():
uuid3 = new_uuid() uuid3 = new_uuid()
uuid4 = new_uuid() uuid4 = new_uuid()
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
xmlns_decl = (
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
format_version = args.FormatVersion
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
# style): платформа держит объявления по алфавиту, дописать в конец нельзя.
if format_rank(format_version) >= 221:
xmlns_decl = xmlns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
xml = f'''<?xml version="1.0" encoding="UTF-8"?> xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject {xmlns_decl} version="{format_version}">
\t<ExternalDataProcessor uuid="{uuid1}"> \t<ExternalDataProcessor uuid="{uuid1}">
\t\t<InternalInfo> \t\t<InternalInfo>
\t\t\t<xr:ContainedObject> \t\t\t<xr:ContainedObject>
@@ -46,11 +121,11 @@ def main():
\t\t\t</xr:GeneratedType> \t\t\t</xr:GeneratedType>
\t\t</InternalInfo> \t\t</InternalInfo>
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name> \t\t\t<Name>{esc_xml_text(name)}</Name>
\t\t\t<Synonym> \t\t\t<Synonym>
\t\t\t\t<v8:item> \t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang> \t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content> \t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>
\t\t\t\t</v8:item> \t\t\t\t</v8:item>
\t\t\t</Synonym> \t\t\t</Synonym>
\t\t\t<Comment/> \t\t\t<Comment/>
@@ -72,7 +147,7 @@ def main():
ext_dir = os.path.join(processor_dir, "Ext") ext_dir = os.path.join(processor_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True) os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml) write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта --- # --- Модуль объекта ---
module_bsl = """\ module_bsl = """\
@@ -89,7 +164,10 @@ def main():
#КонецОбласти""" #КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl") module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl) # Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без).
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
print(f"[OK] Создана обработка: {root_file}") print(f"[OK] Создана обработка: {root_file}")
print(f" Каталог: {processor_dir}") print(f" Каталог: {processor_dir}")
@@ -1,4 +1,4 @@
# epf-validate v1.3 — Validate 1C external data processor / report structure # epf-validate v1.4 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects # Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
param( param(
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-validate v1.3 — Validate 1C external data processor / report structure # epf-validate v1.4 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects # Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
@@ -10,6 +10,28 @@ import sys
from io import StringIO from io import StringIO
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
MD_NS = "http://v8.1c.ru/8.3/MDClasses" MD_NS = "http://v8.1c.ru/8.3/MDClasses"
V8_NS = "http://v8.1c.ru/8.1/data/core" V8_NS = "http://v8.1c.ru/8.1/data/core"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable" XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
@@ -50,7 +72,7 @@ def main():
parser.add_argument("-Detailed", action="store_true") parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30) parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default=None) parser.add_argument("-OutFile", default=None)
args = parser.parse_args() args = ci_parse_args(parser)
max_errors = args.MaxErrors max_errors = args.MaxErrors
+9 -8
View File
@@ -18,20 +18,21 @@ allowed-tools:
## Usage ## Usage
``` ```
/erf-init <Name> [Synonym] [SrcDir] [--with-skd] /erf-init <Name> [Synonym] [SrcDir] [FormatVersion] [--with-skd]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|---------------------------------------| |---------------|:------------:|--------------|---------------------------------------|
| Name | да | — | Имя отчёта (латиница/кириллица) | | Name | да | — | Имя отчёта (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) | | Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD | | SrcDir | нет | `src` | Каталог исходников относительно CWD |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema | | FormatVersion | нет | `2.17` | Версия формата: 2.20 — платформа 8.3.27, 2.21 — 8.5. Дефолт открывается любой платформой |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD] powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<2.17|2.18|2.19|2.20|2.21>"] [-WithSKD]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
+43 -9
View File
@@ -1,4 +1,4 @@
# erf-init v1.1 — Init 1C external report scaffold # erf-init v1.7 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -8,10 +8,22 @@ param(
[string]$SrcDir = "src", [string]$SrcDir = "src",
[switch]$WithSKD [switch]$WithSKD,
# Версия формата выгрузки. Своей конфигурации у автономного отчёта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри отчёта
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
function Esc-XmlText {
param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8 [Console]::InputEncoding = [System.Text.Encoding]::UTF8
@@ -20,6 +32,14 @@ $uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString() $uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString() $uuid4 = [guid]::NewGuid().ToString()
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
# платформа держит объявления по алфавиту, дописать в конец нельзя.
if (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) {
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
# --- Формируем Properties --- # --- Формируем Properties ---
$mainDCSValue = "" $mainDCSValue = ""
@@ -48,7 +68,7 @@ $childObjectsXml = if ($childObjectsContent) {
$xml = @" $xml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject $xmlnsDecl version="$FormatVersion">
<ExternalReport uuid="$uuid1"> <ExternalReport uuid="$uuid1">
<InternalInfo> <InternalInfo>
<xr:ContainedObject> <xr:ContainedObject>
@@ -61,11 +81,11 @@ $xml = @"
</xr:GeneratedType> </xr:GeneratedType>
</InternalInfo> </InternalInfo>
<Properties> <Properties>
<Name>$Name</Name> <Name>$(Esc-XmlText $Name)</Name>
<Synonym> <Synonym>
<v8:item> <v8:item>
<v8:lang>ru</v8:lang> <v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content> <v8:content>$(Esc-XmlText $Synonym)</v8:content>
</v8:item> </v8:item>
</Synonym> </Synonym>
<Comment/> <Comment/>
@@ -98,7 +118,16 @@ $extDir = Join-Path $reportDir "Ext"
New-Item -ItemType Directory -Path $extDir -Force | Out-Null New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) # XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc
# --- Модуль объекта --- # --- Модуль объекта ---
@@ -117,6 +146,11 @@ $moduleBsl = @"
"@ "@
$modulePath = Join-Path $extDir "ObjectModule.bsl" $modulePath = Join-Path $extDir "ObjectModule.bsl"
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
# самого скрипта, а он в репозитории хранится с LF.
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создан отчёт: $rootFile" Write-Host "[OK] Создан отчёт: $rootFile"
@@ -136,7 +170,7 @@ if ($WithSKD) {
$skdMetaXml = @" $skdMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject $xmlnsDecl version="$FormatVersion">
<Template uuid="$skdUuid"> <Template uuid="$skdUuid">
<Properties> <Properties>
<Name>$skdName</Name> <Name>$skdName</Name>
@@ -153,7 +187,7 @@ if ($WithSKD) {
</MetaDataObject> </MetaDataObject>
"@ "@
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc) Write-XmlFile $skdMetaPath $skdMetaXml $enc
$skdContent = @" $skdContent = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
@@ -173,7 +207,7 @@ if ($WithSKD) {
"@ "@
$skdFilePath = Join-Path $skdExtDir "Template.xml" $skdFilePath = Join-Path $skdExtDir "Template.xml"
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc) Write-XmlFile $skdFilePath $skdContent $enc
Write-Host " СКД: $skdMetaPath" Write-Host " СКД: $skdMetaPath"
Write-Host " Тело: $skdFilePath" Write-Host " Тело: $skdFilePath"
+91 -13
View File
@@ -1,19 +1,61 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# erf-init v1.1 — Init 1C external report scaffold # erf-init v1.7 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external report.""" """Generates minimal XML source files for a 1C external report."""
import sys, os, argparse, uuid import sys, os, re, argparse, uuid
def esc_xml(s): # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;') # регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def new_uuid(): def new_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
def write_utf8_bom(path, content): def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -21,8 +63,13 @@ def main():
parser.add_argument('-Name', dest='Name', required=True) parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None) parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src') parser.add_argument('-SrcDir', dest='SrcDir', default='src')
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true') parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
args = parser.parse_args() args = ci_parse_args(parser)
name = args.Name name = args.Name
synonym = args.Synonym if args.Synonym else name synonym = args.Synonym if args.Synonym else name
@@ -33,6 +80,34 @@ def main():
uuid3 = new_uuid() uuid3 = new_uuid()
uuid4 = new_uuid() uuid4 = new_uuid()
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
xmlns_decl = (
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
format_version = args.FormatVersion
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
# style): платформа держит объявления по алфавиту, дописать в конец нельзя.
if format_rank(format_version) >= 221:
xmlns_decl = xmlns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
# --- Properties --- # --- Properties ---
main_dcs_value = "" main_dcs_value = ""
child_objects_content = "" child_objects_content = ""
@@ -45,7 +120,7 @@ def main():
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>" child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
xml = f'''<?xml version="1.0" encoding="UTF-8"?> xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject {xmlns_decl} version="{format_version}">
\t<ExternalReport uuid="{uuid1}"> \t<ExternalReport uuid="{uuid1}">
\t\t<InternalInfo> \t\t<InternalInfo>
\t\t\t<xr:ContainedObject> \t\t\t<xr:ContainedObject>
@@ -58,11 +133,11 @@ def main():
\t\t\t</xr:GeneratedType> \t\t\t</xr:GeneratedType>
\t\t</InternalInfo> \t\t</InternalInfo>
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name> \t\t\t<Name>{esc_xml_text(name)}</Name>
\t\t\t<Synonym> \t\t\t<Synonym>
\t\t\t\t<v8:item> \t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang> \t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content> \t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>
\t\t\t\t</v8:item> \t\t\t\t</v8:item>
\t\t\t</Synonym> \t\t\t</Synonym>
\t\t\t<Comment/> \t\t\t<Comment/>
@@ -90,7 +165,7 @@ def main():
ext_dir = os.path.join(report_dir, "Ext") ext_dir = os.path.join(report_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True) os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml) write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта --- # --- Модуль объекта ---
module_bsl = """\ module_bsl = """\
@@ -107,7 +182,10 @@ def main():
#КонецОбласти""" #КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl") module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl) # Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без).
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
print(f"[OK] Создан отчёт: {root_file}") print(f"[OK] Создан отчёт: {root_file}")
print(f" Каталог: {report_dir}") print(f" Каталог: {report_dir}")
@@ -124,7 +202,7 @@ def main():
skd_uuid = new_uuid() skd_uuid = new_uuid()
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?> skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject {xmlns_decl} version="{format_version}">
\t<Template uuid="{skd_uuid}"> \t<Template uuid="{skd_uuid}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>{skd_name}</Name> \t\t\t<Name>{skd_name}</Name>
@@ -140,7 +218,7 @@ def main():
\t</Template> \t</Template>
</MetaDataObject>''' </MetaDataObject>'''
write_utf8_bom(skd_meta_path, skd_meta_xml) write_xml_file(skd_meta_path, skd_meta_xml)
skd_content = '''<?xml version="1.0" encoding="UTF-8"?> skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" <DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
@@ -158,7 +236,7 @@ def main():
</DataCompositionSchema>''' </DataCompositionSchema>'''
skd_file_path = os.path.join(skd_ext_dir, "Template.xml") skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
write_utf8_bom(skd_file_path, skd_content) write_xml_file(skd_file_path, skd_content)
print(f" СКД: {skd_meta_path}") print(f" СКД: {skd_meta_path}")
print(f" Тело: {skd_file_path}") print(f" Тело: {skd_file_path}")
+85 -15
View File
@@ -1,4 +1,4 @@
# form-add v1.12 — Add managed form to 1C config object # form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -154,6 +154,14 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
function Detect-FormatVersion([string]$dir) { function Detect-FormatVersion([string]$dir) {
$d = $dir $d = $dir
while ($d) { while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml" $cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) { if (Test-Path $cfgPath) {
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8) $cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
@@ -169,6 +177,13 @@ function Detect-FormatVersion([string]$dir) {
return "2.17" return "2.17"
} }
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Фаза 1: Определение типа объекта --- # --- Фаза 1: Определение типа объекта ---
# Resolve ObjectPath (directory → .xml) # Resolve ObjectPath (directory → .xml)
@@ -190,7 +205,26 @@ if (-not (Test-Path $ObjectPath)) {
$objectXmlFull = Resolve-Path $ObjectPath $objectXmlFull = Resolve-Path $ObjectPath
Assert-EditAllowed $objectXmlFull.Path 'editable' Assert-EditAllowed $objectXmlFull.Path 'editable'
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent) # Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
$script:formatVersion = $null
$objHead = [System.IO.File]::ReadAllText($objectXmlFull.Path, [System.Text.Encoding]::UTF8)
$objHead = $objHead.Substring(0, [Math]::Min(2000, $objHead.Length))
if ($objHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { $script:formatVersion = $Matches[1] }
if (-not $script:formatVersion) { $script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent) }
# Объявления пространств имён — одной переменной на корень: места эмиссии их только
# интерполируют. Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
$script:formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
$script:formNsDecl = $script:formNsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
$xmlDoc = New-Object System.Xml.XmlDocument $xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true $xmlDoc.PreserveWhitespace = $true
@@ -313,9 +347,16 @@ if ($objectType -in $processorLikeTypes) {
$extPresentationLine = "`n`t`t`t<ExtendedPresentation/>" $extPresentationLine = "`n`t`t`t<ExtendedPresentation/>"
} }
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
# сразу после UsePurposes (проверено по выгрузке 8.5, до ExtendedPresentation).
$useInIfcLine = ""
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$useInIfcLine = "`n`t`t`t<UseInInterfaceCompatibilityMode>Any</UseInInterfaceCompatibilityMode>"
}
$formMetaXml = @" $formMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$($script:formatVersion)"> <MetaDataObject $($script:xmlnsDecl) version="$($script:formatVersion)">
<Form uuid="$formUuid"> <Form uuid="$formUuid">
<Properties> <Properties>
<Name>$FormName</Name> <Name>$FormName</Name>
@@ -331,20 +372,29 @@ $formMetaXml = @"
<UsePurposes> <UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> <v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value> <v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
</UsePurposes>$extPresentationLine </UsePurposes>$useInIfcLine$extPresentationLine
</Properties> </Properties>
</Form> </Form>
</MetaDataObject> </MetaDataObject>
"@ "@
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom) # XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
#
# Модуль .bsl сюда НЕ идёт — он пишется отдельно.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $formMetaPath $formMetaXml $encBom
# --- 3b. Form.xml --- # --- 3b. Form.xml ---
$formXmlPath = Join-Path $formExtDir "Form.xml" $formXmlPath = Join-Path $formExtDir "Form.xml"
$formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
if ($Purpose -eq "List" -or $Purpose -eq "Choice") { if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
# Динамический список # Динамический список
# MainTable: тип.имя # MainTable: тип.имя
@@ -352,7 +402,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @" $formXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)"> <Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill> <Autofill>true</Autofill>
</AutoCommandBar> </AutoCommandBar>
@@ -377,7 +427,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @" $formXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)"> <Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill> <Autofill>true</Autofill>
</AutoCommandBar> </AutoCommandBar>
@@ -424,7 +474,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @" $formXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)"> <Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill> <Autofill>true</Autofill>
</AutoCommandBar> </AutoCommandBar>
@@ -444,7 +494,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
if (Test-Path $formXmlPath) { if (Test-Path $formXmlPath) {
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting" Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
} else { } else {
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom) Write-XmlFile $formXmlPath $formXml $encBom
} }
# --- 3c. Module.bsl --- # --- 3c. Module.bsl ---
@@ -476,6 +526,11 @@ $moduleBsl = @"
if (Test-Path $modulePath) { if (Test-Path $modulePath) {
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting" Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
} else { } else {
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
# самого скрипта, а он в репозитории хранится с LF.
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom) [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
} }
@@ -585,12 +640,27 @@ if ($SetDefault -or $isFirstFormForPurpose) {
$settings = New-Object System.Xml.XmlWriterSettings $settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom $settings.Encoding = $encBom
$settings.Indent = $false $settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$stream = New-Object System.IO.FileStream($objectXmlFull.Path, [System.IO.FileMode]::Create) # Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$writer = [System.Xml.XmlWriter]::Create($stream, $settings) $memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$xmlDoc.Save($writer) $xmlDoc.Save($writer)
$writer.Close() $writer.Flush(); $writer.Close()
$stream.Close()
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $objectXmlFull.Path) -and ([System.IO.File]::ReadAllText($objectXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($objectXmlFull.Path, $xmlText, $encBom)
# --- Фаза 5: Вывод --- # --- Фаза 5: Вывод ---
+134 -53
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-add v1.12 — Add managed form to 1C config object # form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -11,6 +11,28 @@ import uuid
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================ # ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
@@ -196,6 +218,16 @@ NSMAP = {
def detect_format_version(d): def detect_format_version(d):
while d: while d:
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
ext_path = d + ".xml"
if os.path.isfile(ext_path):
with open(ext_path, "r", encoding="utf-8-sig") as f:
ext_head = f.read(2000)
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
if m:
return m.group(1)
cfg_path = os.path.join(d, "Configuration.xml") cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path): if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f: with open(cfg_path, "r", encoding="utf-8-sig") as f:
@@ -210,6 +242,12 @@ def detect_format_version(d):
return "2.17" return "2.17"
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def _detect_xml_style(path): def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding / """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None файл новый (сохранить текущее поведение).""" финальный перенос. None файл новый (сохранить текущее поведение)."""
@@ -227,21 +265,22 @@ def _detect_xml_style(path):
def _finalize_xml_bytes(xml_bytes, style): def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None).""" """Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
enc_decl = style["enc"] if style else "utf-8" выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace( xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>", b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>') b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах) # Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"") xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n")) .replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть) # Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else True want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n") xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl: if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение) # EOL — как в оригинале (новый файл → CRLF, канон #57)
if style and style["crlf"]: if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n") xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes return xml_bytes
@@ -257,10 +296,24 @@ def save_xml_with_bom(tree, path):
f.write(xml_bytes) f.write(xml_bytes)
def write_text_with_bom(path, text): def write_utf8_bom(path, content):
"""Write text to file with UTF-8 BOM.""" # newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
with open(path, "w", encoding="utf-8-sig") as f: # и LF на macOS, то есть вывод навыка зависел бы от ОС.
f.write(text) with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
Модуль .bsl сюда НЕ идёт он пишется отдельно.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def main(): def main():
@@ -272,7 +325,7 @@ def main():
parser.add_argument("-Synonym", default=None) parser.add_argument("-Synonym", default=None)
parser.add_argument("-Purpose", default="Object") parser.add_argument("-Purpose", default="Object")
parser.add_argument("-SetDefault", action="store_true") parser.add_argument("-SetDefault", action="store_true")
args = parser.parse_args() args = ci_parse_args(parser)
object_path = args.ObjectPath object_path = args.ObjectPath
form_name = args.FormName form_name = args.FormName
@@ -299,7 +352,64 @@ def main():
object_xml_full = os.path.abspath(object_path) object_xml_full = os.path.abspath(object_path)
assert_edit_allowed(object_xml_full, "editable") assert_edit_allowed(object_xml_full, "editable")
format_version = detect_format_version(os.path.dirname(object_xml_full)) # Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
format_version = None
with open(object_xml_full, "r", encoding="utf-8-sig") as f:
obj_head = f.read(2000)
m_ver = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', obj_head)
if m_ver:
format_version = m_ver.group(1)
if not format_version:
format_version = detect_format_version(os.path.dirname(object_xml_full))
# Объявления пространств имён — одной переменной на корень: места эмиссии их только
# подставляют. Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
xmlns_decl = (
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
form_ns_decl = (
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
if format_rank(format_version) >= 221:
pal = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
xmlns_decl = xmlns_decl.replace(' xmlns:style=', pal)
form_ns_decl = form_ns_decl.replace(' xmlns:style=', pal)
parser_xml = etree.XMLParser(remove_blank_text=False) parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(object_xml_full, parser_xml) tree = etree.parse(object_xml_full, parser_xml)
@@ -388,24 +498,7 @@ def main():
form_meta_xml = ( form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n' '<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"' f'<MetaDataObject {xmlns_decl} version="{format_version}">\n'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
f' version="{format_version}">\n'
f'\t<Form uuid="{form_uuid}">\n' f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n' '\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n' f'\t\t\t<Name>{form_name}</Name>\n'
@@ -422,37 +515,22 @@ def main():
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n' '\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n' '\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n' '\t\t\t</UsePurposes>\n'
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
# сразу после UsePurposes (проверено по выгрузке 8.5, до ExtendedPresentation).
+ ('\t\t\t<UseInInterfaceCompatibilityMode>Any</UseInInterfaceCompatibilityMode>\n'
if format_rank(format_version) >= 221 else '')
+ ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '') + ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '')
+ '\t\t</Properties>\n' + '\t\t</Properties>\n'
'\t</Form>\n' '\t</Form>\n'
'</MetaDataObject>' '</MetaDataObject>'
) )
write_text_with_bom(form_meta_path, form_meta_xml) write_xml_file(form_meta_path, form_meta_xml)
# --- 3b. Form.xml --- # --- 3b. Form.xml ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml") form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_ns_decl = (
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
if purpose in ("List", "Choice"): if purpose in ("List", "Choice"):
# Dynamic list # Dynamic list
main_table = f"{object_type}.{object_name}" main_table = f"{object_type}.{object_name}"
@@ -551,7 +629,7 @@ def main():
if os.path.exists(form_xml_path): if os.path.exists(form_xml_path):
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting") print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
else: else:
write_text_with_bom(form_xml_path, form_xml) write_xml_file(form_xml_path, form_xml)
# --- 3c. Module.bsl --- # --- 3c. Module.bsl ---
@@ -582,7 +660,10 @@ def main():
if os.path.exists(module_path): if os.path.exists(module_path):
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting") print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
else: else:
write_text_with_bom(module_path, module_bsl) # Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без).
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
# --- Phase 4: Register in parent object --- # --- Phase 4: Register in parent object ---
@@ -1,4 +1,4 @@
# form-compile v1.176 — Compile 1C managed form from JSON or object metadata # form-compile v1.191 — Compile 1C managed form from JSON or object metadata (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[string]$JsonPath, [string]$JsonPath,
@@ -1335,6 +1335,14 @@ function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) {
function Detect-FormatVersion([string]$dir) { function Detect-FormatVersion([string]$dir) {
$d = $dir $d = $dir
while ($d) { while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml" $cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) { if (Test-Path $cfgPath) {
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8) $cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
@@ -1350,6 +1358,13 @@ function Detect-FormatVersion([string]$dir) {
return "2.17" return "2.17"
} }
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Support guard (Ext/ParentConfigurations.bin) --- # --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / # See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from # read-only configs unless allowed. Trigger = bin present; reaction from
@@ -1485,6 +1500,17 @@ $script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $Ou
Assert-EditAllowed $script:outPathResolved 'editable' Assert-EditAllowed $script:outPathResolved 'editable'
$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved)) $script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved))
# Объявления пространств имён — одной переменной: место эмиссии её только интерполирует.
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
$script:formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$script:formNsDecl = $script:formNsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
# --- 0. Path normalization and mode dispatch --- # --- 0. Path normalization and mode dispatch ---
# Form name → purpose mapping # Form name → purpose mapping
@@ -1679,6 +1705,12 @@ function X {
} }
function Esc-Xml { function Esc-Xml {
param([string]$s)
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
function Esc-XmlText {
# Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > . # Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > .
# Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально); # Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально);
# &quot; ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов. # &quot; ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов.
@@ -1693,14 +1725,14 @@ function Emit-MLItems {
param($val, [string]$indent) param($val, [string]$indent)
if ($val -is [System.Collections.IDictionary]) { if ($val -is [System.Collections.IDictionary]) {
foreach ($k in $val.Keys) { foreach ($k in $val.Keys) {
X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($val[$k])")</v8:content>"; X "$indent</v8:item>" X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($val[$k])")</v8:content>"; X "$indent</v8:item>"
} }
} elseif ($val -is [System.Management.Automation.PSCustomObject]) { } elseif ($val -is [System.Management.Automation.PSCustomObject]) {
foreach ($p in $val.PSObject.Properties) { foreach ($p in $val.PSObject.Properties) {
X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($p.Value)")</v8:content>"; X "$indent</v8:item>" X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($p.Value)")</v8:content>"; X "$indent</v8:item>"
} }
} else { } else {
X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$val")</v8:content>"; X "$indent</v8:item>" X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$val")</v8:content>"; X "$indent</v8:item>"
} }
} }
@@ -1719,7 +1751,7 @@ function Emit-USPresentation {
param($val, [string]$tag, [string]$indent) param($val, [string]$tag, [string]$indent)
if ($null -eq $val) { return } if ($null -eq $val) { return }
if ($val -is [string]) { if ($val -is [string]) {
X "$indent<$tag xsi:type=`"xs:string`">$(Esc-Xml $val)</$tag>" X "$indent<$tag xsi:type=`"xs:string`">$(Esc-XmlText $val)</$tag>"
} else { } else {
Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType" Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType"
} }
@@ -1848,10 +1880,10 @@ function Emit-FilterItem {
} }
} }
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" } if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" } if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$($item.viewMode)")</dcsset:viewMode>" }
if ($item.userSettingID) { if ($item.userSettingID) {
$guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } $guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $guid)</dcsset:userSettingID>" X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $guid)</dcsset:userSettingID>"
} }
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</dcsset:item>" X "$indent</dcsset:item>"
@@ -1859,10 +1891,10 @@ function Emit-FilterItem {
} }
X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">" X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">"
if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" } if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" }
X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml "$($item.field)")</dcsset:left>" X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-XmlText "$($item.field)")</dcsset:left>"
$compType = $script:comparisonTypes["$($item.op)"] $compType = $script:comparisonTypes["$($item.op)"]
if (-not $compType) { $compType = "$($item.op)" } if (-not $compType) { $compType = "$($item.op)" }
X "$indent`t<dcsset:comparisonType>$(Esc-Xml $compType)</dcsset:comparisonType>" X "$indent`t<dcsset:comparisonType>$(Esc-XmlText $compType)</dcsset:comparisonType>"
$valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string]) $valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string])
if ($valIsArray) { if ($valIsArray) {
if (@($item.value).Count -eq 0) { if (@($item.value).Count -eq 0) {
@@ -1881,7 +1913,7 @@ function Emit-FilterItem {
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' } elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' }
else { $vt = 'xs:string' } else { $vt = 'xs:string' }
} }
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" } $vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-XmlText "$v" }
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$v" $nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$v"
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>" X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
} }
@@ -1907,8 +1939,8 @@ function Emit-FilterItem {
$variant = "$sv"; $hasDate = $false; $dateV = $null $variant = "$sv"; $hasDate = $false; $dateV = $null
} }
X "$indent`t<dcsset:right xsi:type=`"v8:$sdType`">" X "$indent`t<dcsset:right xsi:type=`"v8:$sdType`">"
X "$indent`t`t<v8:variant xsi:type=`"v8:${sdType}Variant`">$(Esc-Xml $variant)</v8:variant>" X "$indent`t`t<v8:variant xsi:type=`"v8:${sdType}Variant`">$(Esc-XmlText $variant)</v8:variant>"
if ($hasDate) { X "$indent`t`t<v8:date>$(Esc-Xml $dateV)</v8:date>" } if ($hasDate) { X "$indent`t`t<v8:date>$(Esc-XmlText $dateV)</v8:date>" }
X "$indent`t</dcsset:right>" X "$indent`t</dcsset:right>"
} elseif ("$($item.value)" -eq '_') { } elseif ("$($item.value)" -eq '_') {
# "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right> # "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right>
@@ -1926,15 +1958,15 @@ function Emit-FilterItem {
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" } elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" }
else { $vt = "xs:string" } else { $vt = "xs:string" }
} }
$vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" } $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-XmlText "$($item.value)" }
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$($item.value)" $nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$($item.value)"
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>" X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
} }
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" } if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" } if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$($item.viewMode)")</dcsset:viewMode>" }
if ($item.userSettingID) { if ($item.userSettingID) {
$uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
} }
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</dcsset:item>" X "$indent</dcsset:item>"
@@ -1958,10 +1990,10 @@ function Emit-Filter {
Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t" Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t"
} else { Emit-FilterItem -item $item -indent "$indent`t" } } else { Emit-FilterItem -item $item -indent "$indent`t" }
} }
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
if ($null -ne $blockUserSettingID) { if ($null -ne $blockUserSettingID) {
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
} }
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</dcsset:filter>" X "$indent</dcsset:filter>"
@@ -1983,7 +2015,7 @@ function Emit-Order {
if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" } if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" }
elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" } elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" }
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">" X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" X "$indent`t`t<dcsset:field>$(Esc-XmlText $field)</dcsset:field>"
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>" X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
X "$indent`t</dcsset:item>" X "$indent`t</dcsset:item>"
} }
@@ -1993,16 +2025,16 @@ function Emit-Order {
if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" } if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" }
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">" X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
if ($item.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" } if ($item.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" }
X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>" X "$indent`t`t<dcsset:field>$(Esc-XmlText "$($item.field)")</dcsset:field>"
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>" X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
if ($item.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" } if ($item.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-XmlText "$($item.viewMode)")</dcsset:viewMode>" }
X "$indent`t</dcsset:item>" X "$indent`t</dcsset:item>"
} }
} }
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
if ($null -ne $blockUserSettingID) { if ($null -ne $blockUserSettingID) {
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
} }
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</dcsset:order>" X "$indent</dcsset:order>"
@@ -2034,7 +2066,7 @@ function Emit-AppearanceValue {
if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
} }
if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" } if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" }
X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" X "$indent`t<dcscor:parameter>$(Esc-XmlText $key)</dcscor:parameter>"
$isFontDict = $false $isFontDict = $false
if ($innerVal -is [PSCustomObject]) { if ($innerVal -is [PSCustomObject]) {
$tProp = $innerVal.PSObject.Properties['@type'] $tProp = $innerVal.PSObject.Properties['@type']
@@ -2050,7 +2082,7 @@ function Emit-AppearanceValue {
$lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' } $lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' }
$ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' } $ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' }
X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">" X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">"
X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-Xml $ls)</v8ui:style>" X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-XmlText $ls)</v8ui:style>"
X "$indent`t</dcscor:value>" X "$indent`t</dcscor:value>"
} elseif ($isFontDict) { } elseif ($isFontDict) {
$attrParts = @() $attrParts = @()
@@ -2063,7 +2095,7 @@ function Emit-AppearanceValue {
X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>" X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>"
} elseif ($isDict -and (_HasKey $innerVal 'field')) { } elseif ($isDict -and (_HasKey $innerVal 'field')) {
# Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки
X "$indent`t<dcscor:value xsi:type=`"dcscor:Field`">$(Esc-Xml "$(_Get $innerVal 'field')")</dcscor:value>" X "$indent`t<dcscor:value xsi:type=`"dcscor:Field`">$(Esc-XmlText "$(_Get $innerVal 'field')")</dcscor:value>"
} elseif ($isDict) { } elseif ($isDict) {
# Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value
Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType" Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType"
@@ -2078,19 +2110,19 @@ function Emit-AppearanceValue {
'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType' 'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType'
} }
$keyType = $keyTypeMap[$key] $keyType = $keyTypeMap[$key]
if ($keyType) { X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-Xml $actualVal)</dcscor:value>" } if ($keyType) { X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-XmlText $actualVal)</dcscor:value>" }
elseif ($actualVal -match '^(style|web|win):') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" } elseif ($actualVal -match '^(style|web|win):') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-XmlText $actualVal)</dcscor:value>" }
elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" } elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" }
elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") {
# Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит # Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит
# нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше). # нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше).
# Пустая строка → самозакрывающийся тег (как у платформы). # Пустая строка → самозакрывающийся тег (как у платформы).
if ($actualVal -eq '') { X "$indent`t<dcscor:value xsi:type=`"xs:string`"/>" } if ($actualVal -eq '') { X "$indent`t<dcscor:value xsi:type=`"xs:string`"/>" }
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" } else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText $actualVal)</dcscor:value>" }
} }
elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" } elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" }
elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" } elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-XmlText $actualVal)</dcscor:value>" }
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" } else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText $actualVal)</dcscor:value>" }
} }
if ($nestedItems) { if ($nestedItems) {
$niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null } $niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null }
@@ -2113,7 +2145,7 @@ function Emit-ConditionalAppearance {
X "$indent`t`t<dcsset:selection>" X "$indent`t`t<dcsset:selection>"
foreach ($sel in $ca.selection) { foreach ($sel in $ca.selection) {
X "$indent`t`t`t<dcsset:item>" X "$indent`t`t`t<dcsset:item>"
X "$indent`t`t`t`t<dcsset:field>$(Esc-Xml "$sel")</dcsset:field>" X "$indent`t`t`t`t<dcsset:field>$(Esc-XmlText "$sel")</dcsset:field>"
X "$indent`t`t`t</dcsset:item>" X "$indent`t`t`t</dcsset:item>"
} }
X "$indent`t`t</dcsset:selection>" X "$indent`t`t</dcsset:selection>"
@@ -2132,12 +2164,12 @@ function Emit-ConditionalAppearance {
Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t" Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t"
X "$indent`t`t</dcsset:presentation>" X "$indent`t`t</dcsset:presentation>"
} }
else { X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-Xml "$($ca.presentation)")</dcsset:presentation>" } else { X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-XmlText "$($ca.presentation)")</dcsset:presentation>" }
} }
if ($ca.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($ca.viewMode)")</dcsset:viewMode>" } if ($ca.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-XmlText "$($ca.viewMode)")</dcsset:viewMode>" }
if ($ca.userSettingID) { if ($ca.userSettingID) {
$uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" } $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" }
X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" X "$indent`t`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
} }
if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" } if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) { if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) {
@@ -2153,10 +2185,10 @@ function Emit-ConditionalAppearance {
} }
X "$indent`t</dcsset:item>" X "$indent`t</dcsset:item>"
} }
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
if ($null -ne $blockUserSettingID) { if ($null -ne $blockUserSettingID) {
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
} }
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</$wrapTag>" X "$indent</$wrapTag>"
@@ -2195,14 +2227,14 @@ function Emit-GroupItemField {
$pae = if ($level.periodAdditionEnd) { "$($level.periodAdditionEnd)" } else { '0001-01-01T00:00:00' } $pae = if ($level.periodAdditionEnd) { "$($level.periodAdditionEnd)" } else { '0001-01-01T00:00:00' }
} }
X "$indent<dcsset:item xsi:type=`"dcsset:GroupItemField`">" X "$indent<dcsset:item xsi:type=`"dcsset:GroupItemField`">"
X "$indent`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" X "$indent`t<dcsset:field>$(Esc-XmlText $field)</dcsset:field>"
X "$indent`t<dcsset:groupType>$(Esc-Xml $gt)</dcsset:groupType>" X "$indent`t<dcsset:groupType>$(Esc-XmlText $gt)</dcsset:groupType>"
X "$indent`t<dcsset:periodAdditionType>$(Esc-Xml $pat)</dcsset:periodAdditionType>" X "$indent`t<dcsset:periodAdditionType>$(Esc-XmlText $pat)</dcsset:periodAdditionType>"
# Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field. # Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field.
$pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } $pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
$paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } $paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
X "$indent`t<dcsset:periodAdditionBegin xsi:type=`"$pabT`">$(Esc-Xml $pab)</dcsset:periodAdditionBegin>" X "$indent`t<dcsset:periodAdditionBegin xsi:type=`"$pabT`">$(Esc-XmlText $pab)</dcsset:periodAdditionBegin>"
X "$indent`t<dcsset:periodAdditionEnd xsi:type=`"$paeT`">$(Esc-Xml $pae)</dcsset:periodAdditionEnd>" X "$indent`t<dcsset:periodAdditionEnd xsi:type=`"$paeT`">$(Esc-XmlText $pae)</dcsset:periodAdditionEnd>"
X "$indent</dcsset:item>" X "$indent</dcsset:item>"
} }
@@ -2271,22 +2303,22 @@ function Emit-CalcFields {
} }
$ci = "$indent`t" $ci = "$indent`t"
X "$indent<CalculatedField>" X "$indent<CalculatedField>"
X "$ci<dcssch:dataPath>$(Esc-Xml $dataPath)</dcssch:dataPath>" X "$ci<dcssch:dataPath>$(Esc-XmlText $dataPath)</dcssch:dataPath>"
X "$ci<dcssch:expression>$(Esc-Xml $expression)</dcssch:expression>" X "$ci<dcssch:expression>$(Esc-XmlText $expression)</dcssch:expression>"
if ($title) { Emit-MLText -tag 'dcssch:title' -text $title -indent $ci -xsiType 'v8:LocalStringType' } if ($title) { Emit-MLText -tag 'dcssch:title' -text $title -indent $ci -xsiType 'v8:LocalStringType' }
if ($restrict.Count -gt 0) { if ($restrict.Count -gt 0) {
X "$ci<dcssch:useRestriction>" X "$ci<dcssch:useRestriction>"
foreach ($r in @('field','condition','group','order')) { if ($restrict -contains $r) { X "$ci`t<dcssch:$r>true</dcssch:$r>" } } foreach ($r in @('field','condition','group','order')) { if ($restrict -contains $r) { X "$ci`t<dcssch:$r>true</dcssch:$r>" } }
X "$ci</dcssch:useRestriction>" X "$ci</dcssch:useRestriction>"
} }
if ($pres) { X "$ci<dcssch:presentationExpression>$(Esc-Xml "$pres")</dcssch:presentationExpression>" } if ($pres) { X "$ci<dcssch:presentationExpression>$(Esc-XmlText "$pres")</dcssch:presentationExpression>" }
if ($orderExpr) { if ($orderExpr) {
$oeList = if ($orderExpr -is [System.Collections.IList]) { $orderExpr } else { @($orderExpr) } $oeList = if ($orderExpr -is [System.Collections.IList]) { $orderExpr } else { @($orderExpr) }
foreach ($oe in $oeList) { foreach ($oe in $oeList) {
if ($oe -is [string]) { $exprV = $oe; $oType = 'Asc'; $auto = 'false' } if ($oe -is [string]) { $exprV = $oe; $oType = 'Asc'; $auto = 'false' }
else { $exprV = "$($oe.expression)"; $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' }; $auto = if ($oe.autoOrder) { 'true' } else { 'false' } } else { $exprV = "$($oe.expression)"; $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' }; $auto = if ($oe.autoOrder) { 'true' } else { 'false' } }
X "$ci<dcssch:orderExpression>" X "$ci<dcssch:orderExpression>"
X "$ci`t<expression xmlns=`"$($script:dcsCommonNs)`">$(Esc-Xml $exprV)</expression>" X "$ci`t<expression xmlns=`"$($script:dcsCommonNs)`">$(Esc-XmlText $exprV)</expression>"
X "$ci`t<orderType xmlns=`"$($script:dcsCommonNs)`">$oType</orderType>" X "$ci`t<orderType xmlns=`"$($script:dcsCommonNs)`">$oType</orderType>"
X "$ci`t<autoOrder xmlns=`"$($script:dcsCommonNs)`">$auto</autoOrder>" X "$ci`t<autoOrder xmlns=`"$($script:dcsCommonNs)`">$auto</autoOrder>"
X "$ci</dcssch:orderExpression>" X "$ci</dcssch:orderExpression>"
@@ -2371,26 +2403,49 @@ $script:knownInvalidTypes = @{
"FormTable" = "UI element type, not a data type" "FormTable" = "UI element type, not a data type"
} }
# Алиас на локальный словарь: тело Resolve-TypeStr ниже — общая реализация,
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
$script:typeSynonyms = $script:formTypeSynonyms
function Resolve-TypeStr { function Resolve-TypeStr {
param([string]$typeStr) param([string]$typeStr)
if (-not $typeStr) { return $typeStr } if (-not $typeStr) { return $typeStr }
# Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix)
if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] } # Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
if ($typeStr.StartsWith('cfg:')) {
$typeStr = $typeStr.Substring(4)
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
}
# Параметризованные типы: Number(15,2), Строка(100)
if ($typeStr -match '^([^(]+)\((.+)\)$') { if ($typeStr -match '^([^(]+)\((.+)\)$') {
$base = $Matches[1].Trim(); $params = $Matches[2] $baseName = $Matches[1].Trim()
$r = $script:formTypeSynonyms[$base.ToLower()] $params = $Matches[2]
if ($r) { return "$r($params)" } $resolved = $script:typeSynonyms[$baseName.ToLower()]
if ($resolved) { return "$resolved($params)" }
return $typeStr return $typeStr
} }
# Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
if ($typeStr.Contains('.')) { if ($typeStr.Contains('.')) {
$i = $typeStr.IndexOf('.') $dotIdx = $typeStr.IndexOf('.')
$prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i) $prefix = $typeStr.Substring(0, $dotIdx)
$r = $script:formTypeSynonyms[$prefix.ToLower()] $suffix = $typeStr.Substring($dotIdx) # includes the dot
if ($r) { return "$r$suffix" } $resolved = $script:typeSynonyms[$prefix.ToLower()]
if ($resolved) { return "$resolved$suffix" }
return $typeStr return $typeStr
} }
$r = $script:formTypeSynonyms[$typeStr.ToLower()]
if ($r) { return $r } # Простое имя
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
if ($resolved) { return $resolved }
return $typeStr return $typeStr
} }
@@ -3210,7 +3265,7 @@ function Emit-CommonElementProps {
if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})</$($p[1])>" } if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})</$($p[1])>" }
} }
# Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD) # Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD)
if ($el.headerDataPath) { X "$indent<HeaderDataPath>$(Esc-Xml "$($el.headerDataPath)")</HeaderDataPath>" } if ($el.headerDataPath) { X "$indent<HeaderDataPath>$(Esc-XmlText "$($el.headerDataPath)")</HeaderDataPath>" }
if ($el.footerHorizontalAlign) { X "$indent<FooterHorizontalAlign>$($el.footerHorizontalAlign)</FooterHorizontalAlign>" } if ($el.footerHorizontalAlign) { X "$indent<FooterHorizontalAlign>$($el.footerHorizontalAlign)</FooterHorizontalAlign>" }
if ($el.headerHorizontalAlign) { X "$indent<HeaderHorizontalAlign>$($el.headerHorizontalAlign)</HeaderHorizontalAlign>" } if ($el.headerHorizontalAlign) { X "$indent<HeaderHorizontalAlign>$($el.headerHorizontalAlign)</HeaderHorizontalAlign>" }
# Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD) # Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD)
@@ -3230,8 +3285,8 @@ function Emit-PictureRef {
if (-not $src) { return } if (-not $src) { return }
$srcStr = "$src" $srcStr = "$src"
X "$indent<$picTag>" X "$indent<$picTag>"
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" } if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-XmlText $matches[1])</xr:Abs>" }
else { X "$indent`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" } else { X "$indent`t<xr:Ref>$(Esc-XmlText $srcStr)</xr:Ref>" }
X "$indent`t<xr:LoadTransparent>$(if ($lt) { 'true' } else { 'false' })</xr:LoadTransparent>" X "$indent`t<xr:LoadTransparent>$(if ($lt) { 'true' } else { 'false' })</xr:LoadTransparent>"
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" } if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
X "$indent</$picTag>" X "$indent</$picTag>"
@@ -3260,8 +3315,8 @@ function Emit-CommandPicture {
if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt } if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt }
$srcStr = "$src" $srcStr = "$src"
X "$indent<Picture>" X "$indent<Picture>"
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" } if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-XmlText $matches[1])</xr:Abs>" }
else { X "$indent`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" } else { X "$indent`t<xr:Ref>$(Esc-XmlText $srcStr)</xr:Ref>" }
X "$indent`t<xr:LoadTransparent>$(if ($lt -eq $false) { 'false' } else { 'true' })</xr:LoadTransparent>" X "$indent`t<xr:LoadTransparent>$(if ($lt -eq $false) { 'false' } else { 'true' })</xr:LoadTransparent>"
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" } if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
X "$indent</Picture>" X "$indent</Picture>"
@@ -3423,7 +3478,7 @@ function Emit-GenericScalars {
X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})</$($s.Tag)>" X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})</$($s.Tag)>"
} else { } else {
$v = "$($p.Value)"; if ($v -eq '') { continue } $v = "$($p.Value)"; if ($v -eq '') { continue }
X "$indent<$($s.Tag)>$(Esc-Xml $v)</$($s.Tag)>" X "$indent<$($s.Tag)>$(Esc-XmlText $v)</$($s.Tag)>"
} }
} }
} }
@@ -3470,7 +3525,7 @@ function Emit-BorderTag {
$width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 } $width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 }
$style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null } $style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null }
X "$indent<Border width=`"$width`">" X "$indent<Border width=`"$width`">"
if ($style) { X "$indent`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml $style)</v8ui:style>" } if ($style) { X "$indent`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-XmlText $style)</v8ui:style>" }
X "$indent</Border>" X "$indent</Border>"
} }
@@ -3497,13 +3552,13 @@ function PL-Bool {
} }
function Emit-PlannerColor { function Emit-PlannerColor {
param([string]$tag, $o, [string]$key, [string]$ind) param([string]$tag, $o, [string]$key, [string]$ind)
X "$ind<pl:$tag>$(Esc-Xml "$(PL-Get $o $key 'auto')")</pl:$tag>" X "$ind<pl:$tag>$(Esc-XmlText "$(PL-Get $o $key 'auto')")</pl:$tag>"
} }
# <pl:text>/<pl:tooltip>… — пустое → самозакрывающийся тег (как в выгрузке платформы). # <pl:text>/<pl:tooltip>… — пустое → самозакрывающийся тег (как в выгрузке платформы).
function Emit-PlannerText { function Emit-PlannerText {
param([string]$tag, $v, [string]$ind) param([string]$tag, $v, [string]$ind)
if ([string]::IsNullOrEmpty("$v")) { X "$ind<pl:$tag/>" } if ([string]::IsNullOrEmpty("$v")) { X "$ind<pl:$tag/>" }
else { X "$ind<pl:$tag>$(Esc-Xml "$v")</pl:$tag>" } else { X "$ind<pl:$tag>$(Esc-XmlText "$v")</pl:$tag>" }
} }
# Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef"; # Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef";
# иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы. # иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы.
@@ -3518,7 +3573,7 @@ function Emit-PlannerValue {
param($v, [string]$ind) param($v, [string]$ind)
if ($null -eq $v -or "$v" -eq '') { X "$ind<pl:value xsi:nil=`"true`"/>"; return } if ($null -eq $v -or "$v" -eq '') { X "$ind<pl:value xsi:nil=`"true`"/>"; return }
$t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' } $t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' }
X "$ind<pl:value xsi:type=`"$t`">$(Esc-Xml "$v")</pl:value>" X "$ind<pl:value xsi:type=`"$t`">$(Esc-XmlText "$v")</pl:value>"
} }
function Emit-PlannerFont { function Emit-PlannerFont {
param($o, [string]$ind) param($o, [string]$ind)
@@ -3532,14 +3587,14 @@ function Emit-PlannerBorder {
$bw = if ($b) { PL-Get $b 'width' 1 } else { 1 } $bw = if ($b) { PL-Get $b 'width' 1 } else { 1 }
$bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' } $bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' }
X "$ind<pl:border width=`"$bw`">" X "$ind<pl:border width=`"$bw`">"
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml "$bs")</v8ui:style>" X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-XmlText "$bs")</v8ui:style>"
X "$ind</pl:border>" X "$ind</pl:border>"
} }
function Emit-PlannerLevel { function Emit-PlannerLevel {
param($lv, [string]$cns, [string]$ind) param($lv, [string]$cns, [string]$ind)
$li = "$ind`t" $li = "$ind`t"
X "$ind<level xmlns=`"$cns`">" X "$ind<level xmlns=`"$cns`">"
X "$li<measure>$(Esc-Xml "$(PL-Get $lv 'measure' 'Hour')")</measure>" X "$li<measure>$(Esc-XmlText "$(PL-Get $lv 'measure' 'Hour')")</measure>"
X "$li<interval>$(PL-Get $lv 'interval' 1)</interval>" X "$li<interval>$(PL-Get $lv 'interval' 1)</interval>"
X "$li<show>$(PL-Bool (PL-Get $lv 'show' $true))</show>" X "$li<show>$(PL-Bool (PL-Get $lv 'show' $true))</show>"
$line = PL-Get $lv 'line' $null $line = PL-Get $lv 'line' $null
@@ -3547,10 +3602,10 @@ function Emit-PlannerLevel {
$lg = if ($line) { PL-Get $line 'gap' $false } else { $false } $lg = if ($line) { PL-Get $line 'gap' $false } else { $false }
$lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' } $lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' }
X "$li<line width=`"$lw`" gap=`"$(PL-Bool $lg)`">" X "$li<line width=`"$lw`" gap=`"$(PL-Bool $lg)`">"
X "$li`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-Xml "$lst")</v8ui:style>" X "$li`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-XmlText "$lst")</v8ui:style>"
X "$li</line>" X "$li</line>"
X "$li<scaleColor>$(Esc-Xml "$(PL-Get $lv 'scaleColor' 'auto')")</scaleColor>" X "$li<scaleColor>$(Esc-XmlText "$(PL-Get $lv 'scaleColor' 'auto')")</scaleColor>"
X "$li<dayFormatRule>$(Esc-Xml "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")</dayFormatRule>" X "$li<dayFormatRule>$(Esc-XmlText "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")</dayFormatRule>"
$fmt = PL-Get $lv 'format' $null $fmt = PL-Get $lv 'format' $null
if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } } if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } }
X "$li<format>" X "$li<format>"
@@ -3561,8 +3616,8 @@ function Emit-PlannerLevel {
X "$li<labels>" X "$li<labels>"
X "$li`t<ticks>$ticks</ticks>" X "$li`t<ticks>$ticks</ticks>"
X "$li</labels>" X "$li</labels>"
X "$li<backColor>$(Esc-Xml "$(PL-Get $lv 'backColor' 'auto')")</backColor>" X "$li<backColor>$(Esc-XmlText "$(PL-Get $lv 'backColor' 'auto')")</backColor>"
X "$li<textColor>$(Esc-Xml "$(PL-Get $lv 'textColor' 'auto')")</textColor>" X "$li<textColor>$(Esc-XmlText "$(PL-Get $lv 'textColor' 'auto')")</textColor>"
X "$li<showPereodicalLabels>$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))</showPereodicalLabels>" X "$li<showPereodicalLabels>$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))</showPereodicalLabels>"
X "$ind</level>" X "$ind</level>"
} }
@@ -3571,14 +3626,14 @@ function Emit-PlannerTimeScale {
$cns = $script:CHART_NS $cns = $script:CHART_NS
$ci = "$ind`t" $ci = "$ind`t"
X "$ind<pl:timeScale>" X "$ind<pl:timeScale>"
X "$ci<placement xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")</placement>" X "$ci<placement xmlns=`"$cns`">$(Esc-XmlText "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")</placement>"
$levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() } $levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() }
if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт
foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci } foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci }
$transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false } $transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false }
X "$ci<transparent xmlns=`"$cns`">$(PL-Bool $transp)</transparent>" X "$ci<transparent xmlns=`"$cns`">$(PL-Bool $transp)</transparent>"
X "$ci<backColor xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")</backColor>" X "$ci<backColor xmlns=`"$cns`">$(Esc-XmlText "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")</backColor>"
X "$ci<textColor xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")</textColor>" X "$ci<textColor xmlns=`"$cns`">$(Esc-XmlText "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")</textColor>"
X "$ci<currentLevel xmlns=`"$cns`">$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })</currentLevel>" X "$ci<currentLevel xmlns=`"$cns`">$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })</currentLevel>"
X "$ind</pl:timeScale>" X "$ind</pl:timeScale>"
} }
@@ -3603,7 +3658,7 @@ function Emit-PlannerItem {
X "$ii<pl:id>$id</pl:id>" X "$ii<pl:id>$id</pl:id>"
X "$ii<pl:textFormatted>$(PL-Bool (PL-Get $it 'textFormatted' $false))</pl:textFormatted>" X "$ii<pl:textFormatted>$(PL-Bool (PL-Get $it 'textFormatted' $false))</pl:textFormatted>"
Emit-PlannerBorder $it $ii 'border' Emit-PlannerBorder $it $ii 'border'
X "$ii<pl:editMode>$(Esc-Xml "$(PL-Get $it 'editMode' 'EnableEdit')")</pl:editMode>" X "$ii<pl:editMode>$(Esc-XmlText "$(PL-Get $it 'editMode' 'EnableEdit')")</pl:editMode>"
X "$ind</pl:item>" X "$ind</pl:item>"
} }
# Элемент измерения (<pl:item> внутри <pl:dimension>) — рекурсивен: может нести вложенные # Элемент измерения (<pl:item> внутри <pl:dimension>) — рекурсивен: может нести вложенные
@@ -3658,7 +3713,7 @@ function Emit-PlannerSettings {
$wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null $wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null
if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } } if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } }
Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si
X "$si<pl:periodicVariantUnit>$(Esc-Xml "$(PL-Get $pl 'periodicVariantUnit' 'Day')")</pl:periodicVariantUnit>" X "$si<pl:periodicVariantUnit>$(Esc-XmlText "$(PL-Get $pl 'periodicVariantUnit' 'Day')")</pl:periodicVariantUnit>"
X "$si<pl:periodicVariantRepetition>$(PL-Get $pl 'periodicVariantRepetition' 1)</pl:periodicVariantRepetition>" X "$si<pl:periodicVariantRepetition>$(PL-Get $pl 'periodicVariantRepetition' 1)</pl:periodicVariantRepetition>"
X "$si<pl:timeScaleWrapBeginIndent>$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)</pl:timeScaleWrapBeginIndent>" X "$si<pl:timeScaleWrapBeginIndent>$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)</pl:timeScaleWrapBeginIndent>"
X "$si<pl:timeScaleWrapEndIndent>$(PL-Get $pl 'timeScaleWrapEndIndent' 0)</pl:timeScaleWrapEndIndent>" X "$si<pl:timeScaleWrapEndIndent>$(PL-Get $pl 'timeScaleWrapEndIndent' 0)</pl:timeScaleWrapEndIndent>"
@@ -3671,16 +3726,16 @@ function Emit-PlannerSettings {
X "$si</pl:period>" X "$si</pl:period>"
} }
X "$si<pl:displayCurrentDate>$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))</pl:displayCurrentDate>" X "$si<pl:displayCurrentDate>$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))</pl:displayCurrentDate>"
X "$si<pl:itemsTimeRepresentation>$(Esc-Xml "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")</pl:itemsTimeRepresentation>" X "$si<pl:itemsTimeRepresentation>$(Esc-XmlText "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")</pl:itemsTimeRepresentation>"
X "$si<pl:itemsBehaviorWhenSpaceInsufficient>$(Esc-Xml "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")</pl:itemsBehaviorWhenSpaceInsufficient>" X "$si<pl:itemsBehaviorWhenSpaceInsufficient>$(Esc-XmlText "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")</pl:itemsBehaviorWhenSpaceInsufficient>"
X "$si<pl:autoMinColumnWidth>$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))</pl:autoMinColumnWidth>" X "$si<pl:autoMinColumnWidth>$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))</pl:autoMinColumnWidth>"
X "$si<pl:autoMinRowHeight>$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))</pl:autoMinRowHeight>" X "$si<pl:autoMinRowHeight>$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))</pl:autoMinRowHeight>"
X "$si<pl:minColumnWidth>$(PL-Get $pl 'minColumnWidth' 0)</pl:minColumnWidth>" X "$si<pl:minColumnWidth>$(PL-Get $pl 'minColumnWidth' 0)</pl:minColumnWidth>"
X "$si<pl:minRowHeight>$(PL-Get $pl 'minRowHeight' 0)</pl:minRowHeight>" X "$si<pl:minRowHeight>$(PL-Get $pl 'minRowHeight' 0)</pl:minRowHeight>"
X "$si<pl:fixDimensionsHeader>$(Esc-Xml "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")</pl:fixDimensionsHeader>" X "$si<pl:fixDimensionsHeader>$(Esc-XmlText "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")</pl:fixDimensionsHeader>"
X "$si<pl:fixTimeScaleHeader>$(Esc-Xml "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")</pl:fixTimeScaleHeader>" X "$si<pl:fixTimeScaleHeader>$(Esc-XmlText "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")</pl:fixTimeScaleHeader>"
Emit-PlannerBorder $pl $si 'border' Emit-PlannerBorder $pl $si 'border'
X "$si<pl:newItemsTextType>$(Esc-Xml "$(PL-Get $pl 'newItemsTextType' 'String')")</pl:newItemsTextType>" X "$si<pl:newItemsTextType>$(Esc-XmlText "$(PL-Get $pl 'newItemsTextType' 'String')")</pl:newItemsTextType>"
X "$ind</Settings>" X "$ind</Settings>"
} }
@@ -3713,13 +3768,13 @@ function Emit-ChartNode {
if ($keys -contains 'gap') { if ($keys -contains 'gap') {
$w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style' $w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style'
X "$ind<d4p1:$name width=`"$w`" gap=`"$(PL-Bool $g)`">" X "$ind<d4p1:$name width=`"$w`" gap=`"$(PL-Bool $g)`">"
X "$ind`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-Xml "$st")</v8ui:style>" X "$ind`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-XmlText "$st")</v8ui:style>"
X "$ind</d4p1:$name>"; return X "$ind</d4p1:$name>"; return
} }
if (($keys -contains 'style') -and ($keys -contains 'width')) { if (($keys -contains 'style') -and ($keys -contains 'width')) {
$w = Get-Prop $val 'width'; $st = Get-Prop $val 'style' $w = Get-Prop $val 'width'; $st = Get-Prop $val 'style'
X "$ind<d4p1:$name width=`"$w`">" X "$ind<d4p1:$name width=`"$w`">"
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml "$st")</v8ui:style>" X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-XmlText "$st")</v8ui:style>"
X "$ind</d4p1:$name>"; return X "$ind</d4p1:$name>"; return
} }
$isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } } $isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } }
@@ -3735,7 +3790,7 @@ function Emit-ChartNode {
} }
if ($null -eq $val -or "$val" -eq '') { X "$ind<d4p1:$name/>"; return } if ($null -eq $val -or "$val" -eq '') { X "$ind<d4p1:$name/>"; return }
if ($val -is [bool]) { X "$ind<d4p1:$name>$(PL-Bool $val)</d4p1:$name>"; return } if ($val -is [bool]) { X "$ind<d4p1:$name>$(PL-Bool $val)</d4p1:$name>"; return }
X "$ind<d4p1:$name>$(Esc-Xml "$val")</d4p1:$name>" X "$ind<d4p1:$name>$(Esc-XmlText "$val")</d4p1:$name>"
} }
function Emit-ChartSettings { function Emit-ChartSettings {
param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart') param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart')
@@ -3757,7 +3812,7 @@ function Emit-Appearance {
if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue } if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue }
$spec = $script:appearanceSpec[$key] $spec = $script:appearanceSpec[$key]
switch ($spec.kind) { switch ($spec.kind) {
'color' { X "$indent<$($spec.tag)>$(Esc-Xml "$val")</$($spec.tag)>" } 'color' { X "$indent<$($spec.tag)>$(Esc-XmlText "$val")</$($spec.tag)>" }
'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent } 'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent }
'border' { Emit-BorderTag -val $val -indent $indent } 'border' { Emit-BorderTag -val $val -indent $indent }
} }
@@ -4042,13 +4097,13 @@ function Emit-Input {
@('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'), @('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'),
@('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath') @('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath')
)) { )) {
if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-Xml "$($el.($p[0]))")</$($p[1])>" } if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-XmlText "$($el.($p[0]))")</$($p[1])>" }
} }
# MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). # MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором).
foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) { foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) {
if ($null -ne $el.($p[0])) { if ($null -ne $el.($p[0])) {
$mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' } $mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' }
X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-Xml "$($el.($p[0]))")</$($p[1])>" X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-XmlText "$($el.($p[0]))")</$($p[1])>"
} }
} }
if ($el.choiceButtonRepresentation) { X "$inner<ChoiceButtonRepresentation>$($el.choiceButtonRepresentation)</ChoiceButtonRepresentation>" } if ($el.choiceButtonRepresentation) { X "$inner<ChoiceButtonRepresentation>$($el.choiceButtonRepresentation)</ChoiceButtonRepresentation>" }
@@ -4111,7 +4166,7 @@ function Emit-Check {
if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner }
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" } if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-XmlText "$($el.footerDataPath)")</FooterDataPath>" }
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
# Формат / формат редактирования (LocalStringType — строка или {ru,en}) # Формат / формат редактирования (LocalStringType — строка или {ru,en})
@@ -4268,7 +4323,7 @@ function Emit-ChoicePresentation {
foreach ($pair in $pairs) { foreach ($pair in $pairs) {
X "$indent`t<v8:item>" X "$indent`t<v8:item>"
X "$indent`t`t<v8:lang>$($pair[0])</v8:lang>" X "$indent`t`t<v8:lang>$($pair[0])</v8:lang>"
X "$indent`t`t<v8:content>$(Esc-Xml $pair[1])</v8:content>" X "$indent`t`t<v8:content>$(Esc-XmlText $pair[1])</v8:content>"
X "$indent`t</v8:item>" X "$indent`t</v8:item>"
} }
X "$indent</Presentation>" X "$indent</Presentation>"
@@ -4278,7 +4333,7 @@ function Emit-ChoicePresentation {
function Get-ChoiceValueTag { function Get-ChoiceValueTag {
param($norm) param($norm)
if ([string]::IsNullOrEmpty($norm.Text)) { return "<Value xsi:type=`"$($norm.XsiType)`"/>" } if ([string]::IsNullOrEmpty($norm.Text)) { return "<Value xsi:type=`"$($norm.XsiType)`"/>" }
return "<Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</Value>" return "<Value xsi:type=`"$($norm.XsiType)`">$(Esc-XmlText $norm.Text)</Value>"
} }
# Emit <ChoiceList> (список выбора) — у RadioButtonField и InputField. # Emit <ChoiceList> (список выбора) — у RadioButtonField и InputField.
@@ -4491,8 +4546,8 @@ function Emit-ChoiceParameterLinks {
} }
} }
X "$indent`t<xr:Link>" X "$indent`t<xr:Link>"
X "$indent`t`t<xr:Name>$(Esc-Xml "$name")</xr:Name>" X "$indent`t`t<xr:Name>$(Esc-XmlText "$name")</xr:Name>"
X "$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-Xml "$dp")</xr:DataPath>" X "$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-XmlText "$dp")</xr:DataPath>"
X "$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>" X "$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>"
X "$indent`t</xr:Link>" X "$indent`t</xr:Link>"
} }
@@ -4509,7 +4564,7 @@ function Emit-TypeLink {
$li = Get-ElProp $tl @('linkItem','элементСвязи') $li = Get-ElProp $tl @('linkItem','элементСвязи')
if ($null -eq $li) { $li = 0 } if ($null -eq $li) { $li = 0 }
X "$indent<TypeLink>" X "$indent<TypeLink>"
X "$indent`t<xr:DataPath>$(Esc-Xml "$dp")</xr:DataPath>" X "$indent`t<xr:DataPath>$(Esc-XmlText "$dp")</xr:DataPath>"
X "$indent`t<xr:LinkItem>$li</xr:LinkItem>" X "$indent`t<xr:LinkItem>$li</xr:LinkItem>"
X "$indent</TypeLink>" X "$indent</TypeLink>"
} }
@@ -4616,7 +4671,7 @@ function Emit-LabelField {
if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" } if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" } if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
# FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode # FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" } if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-XmlText "$($el.footerDataPath)")</FooterDataPath>" }
# PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение # PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение
if ($null -ne $el.passwordMode) { X "$inner<PasswordMode>$(if ($el.passwordMode){'true'}else{'false'})</PasswordMode>" } if ($null -ne $el.passwordMode) { X "$inner<PasswordMode>$(if ($el.passwordMode){'true'}else{'false'})</PasswordMode>" }
Emit-ColumnPics -el $el -indent $inner Emit-ColumnPics -el $el -indent $inner
@@ -4939,7 +4994,7 @@ function Emit-Button {
if (($btnParam -is [System.Management.Automation.PSCustomObject] -or $btnParam -is [hashtable]) -and $btnParam.type) { if (($btnParam -is [System.Management.Automation.PSCustomObject] -or $btnParam -is [hashtable]) -and $btnParam.type) {
Emit-Type -typeStr "$($btnParam.type)" -indent $inner -tag "Parameter" -tagAttrs ' xsi:type="v8:TypeDescription"' Emit-Type -typeStr "$($btnParam.type)" -indent $inner -tag "Parameter" -tagAttrs ' xsi:type="v8:TypeDescription"'
} else { } else {
X "$inner<Parameter xsi:type=`"xr:MDObjectRef`">$(Esc-Xml "$btnParam")</Parameter>" X "$inner<Parameter xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText "$btnParam")</Parameter>"
} }
} }
# DataPath — привязка команды кнопки к контексту (Объект.Ref, Items.X.CurrentData.Поле) # DataPath — привязка команды кнопки к контексту (Объект.Ref, Items.X.CurrentData.Поле)
@@ -4994,8 +5049,8 @@ function Emit-PictureDecoration {
$srcStr = "$($el.src)" $srcStr = "$($el.src)"
$lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" } $lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" }
X "$inner<Picture>" X "$inner<Picture>"
if ($srcStr -match '^abs:(.*)$') { X "$inner`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" } if ($srcStr -match '^abs:(.*)$') { X "$inner`t<xr:Abs>$(Esc-XmlText $matches[1])</xr:Abs>" }
else { X "$inner`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" } else { X "$inner`t<xr:Ref>$(Esc-XmlText $srcStr)</xr:Ref>" }
X "$inner`t<xr:LoadTransparent>$lt</xr:LoadTransparent>" X "$inner`t<xr:LoadTransparent>$lt</xr:LoadTransparent>"
if ($el.transparentPixel) { X "$inner`t<xr:TransparentPixel x=`"$($el.transparentPixel.x)`" y=`"$($el.transparentPixel.y)`"/>" } if ($el.transparentPixel) { X "$inner`t<xr:TransparentPixel x=`"$($el.transparentPixel.x)`" y=`"$($el.transparentPixel.y)`"/>" }
X "$inner</Picture>" X "$inner</Picture>"
@@ -5039,7 +5094,7 @@ function Emit-PictureField {
if ($null -ne $el.enableDrag) { X "$inner<EnableDrag>$(if ($el.enableDrag){'true'}else{'false'})</EnableDrag>" } if ($null -ne $el.enableDrag) { X "$inner<EnableDrag>$(if ($el.enableDrag){'true'}else{'false'})</EnableDrag>" }
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" } if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-XmlText "$($el.footerDataPath)")</FooterDataPath>" }
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
# ValuesPicture — picture (collection) used to render the field's value. # ValuesPicture — picture (collection) used to render the field's value.
@@ -5395,18 +5450,18 @@ function Emit-DLValue {
return return
} }
$valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" } $valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" }
if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" } if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-XmlText $valStr)</dcssch:value>" }
elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" } elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-XmlText $valStr)</dcssch:value>" }
elseif ($type -eq 'v8:Type') { $nsAttr = Get-ValueTypeNsAttr -valueType 'v8:Type' -value $valStr; X "$indent<dcssch:value$nsAttr xsi:type=`"v8:Type`">$(Esc-Xml $valStr)</dcssch:value>" } elseif ($type -eq 'v8:Type') { $nsAttr = Get-ValueTypeNsAttr -valueType 'v8:Type' -value $valStr; X "$indent<dcssch:value$nsAttr xsi:type=`"v8:Type`">$(Esc-XmlText $valStr)</dcssch:value>" }
elseif ($type -match '^ent:') { X "$indent<dcssch:value xsi:type=`"$type`">$(Esc-Xml $valStr)</dcssch:value>" } # системное перечисление (ent:X) — value несёт тот же xsi:type elseif ($type -match '^ent:') { X "$indent<dcssch:value xsi:type=`"$type`">$(Esc-XmlText $valStr)</dcssch:value>" } # системное перечисление (ent:X) — value несёт тот же xsi:type
elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</dcssch:value>" } elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-XmlText $valStr)</dcssch:value>" }
elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" } elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-XmlText $valStr)</dcssch:value>" }
elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" } elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText $valStr)</dcssch:value>" }
else { else {
if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" } if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-XmlText $valStr)</dcssch:value>" }
elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" } elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-XmlText $valStr)</dcssch:value>" }
elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" } elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText $valStr)</dcssch:value>" }
else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" } else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-XmlText $valStr)</dcssch:value>" }
} }
} }
@@ -5440,7 +5495,7 @@ function Emit-DLInputParameters {
foreach ($item in $items) { foreach ($item in $items) {
X "$indent`t<dcscor:item>" X "$indent`t<dcscor:item>"
if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($item.parameter)")</dcscor:parameter>" X "$indent`t`t<dcscor:parameter>$(Esc-XmlText "$($item.parameter)")</dcscor:parameter>"
if (Has-DLProp $item 'choiceParameters') { if (Has-DLProp $item 'choiceParameters') {
$cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() } $cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() }
if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" } if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" }
@@ -5448,11 +5503,11 @@ function Emit-DLInputParameters {
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">" X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">"
foreach ($cpItem in $cpItems) { foreach ($cpItem in $cpItems) {
X "$indent`t`t`t<dcscor:item>" X "$indent`t`t`t<dcscor:item>"
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cpItem.name)")</dcscor:choiceParameter>" X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-XmlText "$($cpItem.name)")</dcscor:choiceParameter>"
foreach ($v in @($cpItem.values)) { foreach ($v in @($cpItem.values)) {
if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" } if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" }
elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" } elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" }
else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$v")</dcscor:value>" } else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText "$v")</dcscor:value>" }
} }
X "$indent`t`t`t</dcscor:item>" X "$indent`t`t`t</dcscor:item>"
} }
@@ -5465,8 +5520,8 @@ function Emit-DLInputParameters {
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">" X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">"
foreach ($cplItem in $cplItems) { foreach ($cplItem in $cplItems) {
X "$indent`t`t`t<dcscor:item>" X "$indent`t`t`t<dcscor:item>"
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cplItem.name)")</dcscor:choiceParameter>" X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-XmlText "$($cplItem.name)")</dcscor:choiceParameter>"
X "$indent`t`t`t`t<dcscor:value>$(Esc-Xml "$($cplItem.value)")</dcscor:value>" X "$indent`t`t`t`t<dcscor:value>$(Esc-XmlText "$($cplItem.value)")</dcscor:value>"
$mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' } $mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' }
X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>" X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>"
X "$indent`t`t`t</dcscor:item>" X "$indent`t`t`t</dcscor:item>"
@@ -5477,15 +5532,15 @@ function Emit-DLInputParameters {
# Связь по типу (dcscor:TypeLink) — field + linkItem (структурное значение параметра). # Связь по типу (dcscor:TypeLink) — field + linkItem (структурное значение параметра).
$tl = $item.typeLink $tl = $item.typeLink
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:TypeLink`">" X "$indent`t`t<dcscor:value xsi:type=`"dcscor:TypeLink`">"
$tlf = Get-Prop $tl 'field'; if ($null -ne $tlf) { X "$indent`t`t`t<dcscor:field>$(Esc-Xml "$tlf")</dcscor:field>" } $tlf = Get-Prop $tl 'field'; if ($null -ne $tlf) { X "$indent`t`t`t<dcscor:field>$(Esc-XmlText "$tlf")</dcscor:field>" }
$tli = Get-Prop $tl 'linkItem'; if ($null -ne $tli) { X "$indent`t`t`t<dcscor:linkItem>$(Esc-Xml "$tli")</dcscor:linkItem>" } $tli = Get-Prop $tl 'linkItem'; if ($null -ne $tli) { X "$indent`t`t`t<dcscor:linkItem>$(Esc-XmlText "$tli")</dcscor:linkItem>" }
X "$indent`t`t</dcscor:value>" X "$indent`t`t</dcscor:value>"
} elseif (Has-DLProp $item 'value') { } elseif (Has-DLProp $item 'value') {
$val = $item.value $val = $item.value
if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" } if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" }
elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" } elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" }
elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" } elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" }
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$val")</dcscor:value>" } else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText "$val")</dcscor:value>" }
} }
X "$indent`t</dcscor:item>" X "$indent`t</dcscor:item>"
} }
@@ -5560,16 +5615,16 @@ function Emit-DataParameters {
} }
X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
if ($dp.use -eq $false) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } if ($dp.use -eq $false) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>" X "$indent`t`t<dcscor:parameter>$(Esc-XmlText "$($dp.parameter)")</dcscor:parameter>"
$dpValIsArr = ($dp.value -is [array]) -or ($dp.value -is [System.Collections.IList] -and $dp.value -isnot [string]) $dpValIsArr = ($dp.value -is [array]) -or ($dp.value -is [System.Collections.IList] -and $dp.value -isnot [string])
if ($dpValIsArr) { if ($dpValIsArr) {
# Список значений параметра (valueListAllowed) — отдельный <dcscor:value> на каждое. # Список значений параметра (valueListAllowed) — отдельный <dcscor:value> на каждое.
$avtype = "$($dp.valueType)" $avtype = "$($dp.valueType)"
foreach ($v in @($dp.value)) { foreach ($v in @($dp.value)) {
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { "$v" } $vStr = if ($v -is [bool]) { "$v".ToLower() } else { "$v" }
if ($avtype -match '^[a-zA-Z]+:') { X "$indent`t`t<dcscor:value xsi:type=`"$avtype`">$(Esc-Xml $vStr)</dcscor:value>" } if ($avtype -match '^[a-zA-Z]+:') { X "$indent`t`t<dcscor:value xsi:type=`"$avtype`">$(Esc-XmlText $vStr)</dcscor:value>" }
elseif ("$vStr" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$vStr" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $vStr)</dcscor:value>" } elseif ("$vStr" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$vStr" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText $vStr)</dcscor:value>" }
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $vStr)</dcscor:value>" } else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText $vStr)</dcscor:value>" }
} }
} elseif ($dp.nilValue -eq $true) { } elseif ($dp.nilValue -eq $true) {
X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>" X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>"
@@ -5592,41 +5647,41 @@ function Emit-DataParameters {
if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" } if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" }
elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" } elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" }
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">" X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">"
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-Xml $_variantStr)</v8:variant>" X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-XmlText $_variantStr)</v8:variant>"
if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-Xml $_d)</v8:date>" } if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-XmlText $_d)</v8:date>" }
X "$indent`t`t</dcscor:value>" X "$indent`t`t</dcscor:value>"
} else { } else {
$_sd = $null; $_ed = $null $_sd = $null; $_ed = $null
if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } } if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } }
else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } } else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } }
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">" X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $_variantStr)</v8:variant>" X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-XmlText $_variantStr)</v8:variant>"
if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-Xml $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-Xml $_ed)</v8:endDate>" } if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-XmlText $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-XmlText $_ed)</v8:endDate>" }
X "$indent`t`t</dcscor:value>" X "$indent`t`t</dcscor:value>"
} }
} elseif ($vtype -match '^[a-zA-Z]+:') { } elseif ($vtype -match '^[a-zA-Z]+:') {
$vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" } $vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" }
X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-Xml $vStr)</dcscor:value>" X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-XmlText $vStr)</dcscor:value>"
} elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) { } elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) {
X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml ("$($dp.value)".ToLower()))</dcscor:value>" X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-XmlText ("$($dp.value)".ToLower()))</dcscor:value>"
} elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { } elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') {
X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>" X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
} elseif ($vtype -match '^decimal') { } elseif ($vtype -match '^decimal') {
X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>" X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
} elseif ($vtype -match '^string') { } elseif ($vtype -match '^string') {
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
} elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { } elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$($dp.value)")</dcscor:value>" X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
} else { } else {
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
} }
} }
if ($dp.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($dp.viewMode)")</dcsset:viewMode>" } if ($dp.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-XmlText "$($dp.viewMode)")</dcsset:viewMode>" }
if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" } if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>" }
if ($dp.userSettingPresentation) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" } if ($dp.userSettingPresentation) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
X "$indent`t</dcscor:item>" X "$indent`t</dcscor:item>"
} }
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
X "$indent</dcsset:dataParameters>" X "$indent</dcsset:dataParameters>"
} }
@@ -5634,7 +5689,7 @@ function Emit-DLParameter {
param($p, $parsed, [string]$indent) param($p, $parsed, [string]$indent)
X "$indent<Parameter>" X "$indent<Parameter>"
$ci = "$indent`t" $ci = "$indent`t"
X "$ci<dcssch:name>$(Esc-Xml $parsed.name)</dcssch:name>" X "$ci<dcssch:name>$(Esc-XmlText $parsed.name)</dcssch:name>"
# Title: явный override (shorthand [..] / объект title/presentation) или авто из имени. # Title: явный override (shorthand [..] / объект title/presentation) или авто из имени.
$title = $null $title = $null
if ($parsed.title) { $title = $parsed.title } if ($parsed.title) { $title = $parsed.title }
@@ -5668,7 +5723,7 @@ function Emit-DLParameter {
# expression # expression
$expr = $null $expr = $null
if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" } if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" }
if ($expr) { X "$ci<dcssch:expression>$(Esc-Xml $expr)</dcssch:expression>" } if ($expr) { X "$ci<dcssch:expression>$(Esc-XmlText $expr)</dcssch:expression>" }
# availableValues # availableValues
if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) { if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) {
foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci } foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci }
@@ -5687,7 +5742,7 @@ function Emit-DLParameter {
# use # use
$useVal = $null $useVal = $null
if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" } if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" }
if ($useVal) { X "$ci<dcssch:use>$(Esc-Xml $useVal)</dcssch:use>" } if ($useVal) { X "$ci<dcssch:use>$(Esc-XmlText $useVal)</dcssch:use>" }
X "$indent</Parameter>" X "$indent</Parameter>"
} }
@@ -5807,7 +5862,7 @@ function Emit-Attributes {
} }
if ($saveFields.Count -gt 0) { if ($saveFields.Count -gt 0) {
X "$inner<Save>" X "$inner<Save>"
foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-Xml $f)</Field>" } foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-XmlText $f)</Field>" }
X "$inner</Save>" X "$inner</Save>"
} }
} }
@@ -5908,7 +5963,7 @@ function Emit-Attributes {
X "$si<DynamicDataRead>$ddr</DynamicDataRead>" X "$si<DynamicDataRead>$ddr</DynamicDataRead>"
if ($hasQuery) { if ($hasQuery) {
$qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir $qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir
X "$si<QueryText>$(Esc-Xml $qtext)</QueryText>" X "$si<QueryText>$(Esc-XmlText $qtext)</QueryText>"
} }
# Явные поля набора (редко): override title/dataPath # Явные поля набора (редко): override title/dataPath
if ($st.fields) { if ($st.fields) {
@@ -5923,8 +5978,8 @@ function Emit-Attributes {
if ($null -ne (Get-Prop $fld 'dataPath')) { $dp = "$($fld.dataPath)" } if ($null -ne (Get-Prop $fld 'dataPath')) { $dp = "$($fld.dataPath)" }
elseif ($isFolder) { $dp = "" } elseif ($isFolder) { $dp = "" }
else { $dp = "$($fld.field)" } else { $dp = "$($fld.field)" }
if ($dp -eq "") { X "$si`t<dcssch:dataPath/>" } else { X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>" } if ($dp -eq "") { X "$si`t<dcssch:dataPath/>" } else { X "$si`t<dcssch:dataPath>$(Esc-XmlText "$dp")</dcssch:dataPath>" }
if (-not $isFolder) { X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>" } if (-not $isFolder) { X "$si`t<dcssch:field>$(Esc-XmlText "$($fld.field)")</dcssch:field>" }
if ($fld.title) { if ($fld.title) {
X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">" X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">"
Emit-MLItems -val $fld.title -indent "$si`t`t" Emit-MLItems -val $fld.title -indent "$si`t`t"
@@ -5934,7 +5989,7 @@ function Emit-Attributes {
Emit-RestrictBlock 'useRestriction' $fld.useRestriction "$si`t" Emit-RestrictBlock 'useRestriction' $fld.useRestriction "$si`t"
Emit-RestrictBlock 'attributeUseRestriction' $fld.attributeUseRestriction "$si`t" Emit-RestrictBlock 'attributeUseRestriction' $fld.attributeUseRestriction "$si`t"
# presentationExpression поля — перед valueType (порядок исходника) # presentationExpression поля — перед valueType (порядок исходника)
if ($fld.presentationExpression) { X "$si`t<dcssch:presentationExpression>$(Esc-Xml "$($fld.presentationExpression)")</dcssch:presentationExpression>" } if ($fld.presentationExpression) { X "$si`t<dcssch:presentationExpression>$(Esc-XmlText "$($fld.presentationExpression)")</dcssch:presentationExpression>" }
# valueType поля набора (тип значения; вычисляемые/кастомные поля) # valueType поля набора (тип значения; вычисляемые/кастомные поля)
if ($fld.valueType) { Emit-DLValueType -typeStr "$($fld.valueType)" -indent "$si`t" } if ($fld.valueType) { Emit-DLValueType -typeStr "$($fld.valueType)" -indent "$si`t" }
# appearance поля (формат/оформление) — после valueType (порядок исходника) # appearance поля (формат/оформление) — после valueType (порядок исходника)
@@ -5954,8 +6009,8 @@ function Emit-Attributes {
Emit-DLParameters -params $st.parameters -indent $si Emit-DLParameters -params $st.parameters -indent $si
# Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey) # Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey)
# + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений. # + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений.
if ($st.keyType) { X "$si<KeyType>$(Esc-Xml "$($st.keyType)")</KeyType>" } if ($st.keyType) { X "$si<KeyType>$(Esc-XmlText "$($st.keyType)")</KeyType>" }
if ($st.keyFields) { foreach ($kf in @($st.keyFields)) { X "$si<KeyField>$(Esc-Xml "$kf")</KeyField>" } } if ($st.keyFields) { foreach ($kf in @($st.keyFields)) { X "$si<KeyField>$(Esc-XmlText "$kf")</KeyField>" } }
if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" } if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" }
# GetInvisibleFieldPresentations — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении false). # GetInvisibleFieldPresentations — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении false).
if ($null -ne $st.getInvisibleFieldPresentations) { X "$si<GetInvisibleFieldPresentations>$(if ($st.getInvisibleFieldPresentations){'true'}else{'false'})</GetInvisibleFieldPresentations>" } if ($null -ne $st.getInvisibleFieldPresentations) { X "$si<GetInvisibleFieldPresentations>$(if ($st.getInvisibleFieldPresentations){'true'}else{'false'})</GetInvisibleFieldPresentations>" }
@@ -6092,7 +6147,7 @@ function Emit-Commands {
if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId } if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId }
if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица } if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица }
if ($cmdTable) { if ($cmdTable) {
X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-Xml "$cmdTable")</AssociatedTableElementId>" X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-XmlText "$cmdTable")</AssociatedTableElementId>"
} }
if ($cmd.shortcut) { if ($cmd.shortcut) {
@@ -6184,10 +6239,10 @@ function Emit-CommandInterface {
# group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк # group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк
if ($treeGroup) { $grp = $treeGroup } if ($treeGroup) { $grp = $treeGroup }
X "$inner`t<Item>" X "$inner`t<Item>"
X "$inner`t`t<Command>$(Esc-Xml "$cmd")</Command>" X "$inner`t`t<Command>$(Esc-XmlText "$cmd")</Command>"
X "$inner`t`t<Type>$type</Type>" X "$inner`t`t<Type>$type</Type>"
if ($attr) { X "$inner`t`t<Attribute>$(Esc-Xml "$attr")</Attribute>" } if ($attr) { X "$inner`t`t<Attribute>$(Esc-XmlText "$attr")</Attribute>" }
if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-Xml "$grp")</CommandGroup>" } if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-XmlText "$grp")</CommandGroup>" }
if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" } if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" }
if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" } if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" }
if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" } if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" }
@@ -6463,25 +6518,14 @@ function Compute-MainAcbAutofill {
# --- 12. Main compilation --- # --- 12. Main compilation ---
# Title # Буфер и счётчики — с чистого листа: до этой точки они могли быть тронуты режимом from-object.
if ($def.title) {
Emit-MLText -tag "Title" -text $def.title -indent "`t"
}
# Header
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">"
# Oops — Title was emitted before header. Need to fix the order.
# Actually, let me restructure: build the body into a separate buffer, then assemble
# Reset and rebuild properly
$script:xml = New-Object System.Text.StringBuilder 8192 $script:xml = New-Object System.Text.StringBuilder 8192
$script:nextId = 1 $script:nextId = 1
$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме) $script:seenElementNames = @{} # пул имён элементов (глобально по всей форме)
# Header
X '<?xml version="1.0" encoding="UTF-8"?>' X '<?xml version="1.0" encoding="UTF-8"?>'
X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">" X "<Form $($script:formNsDecl) version=`"$($script:formatVersion)`">"
# 12a. Title (from def.title or properties.title — must be multilingual XML) # 12a. Title (from def.title or properties.title — must be multilingual XML)
$formTitle = $def.title $formTitle = $def.title
@@ -6535,7 +6579,7 @@ if ($null -ne $def.mobileCommandBarContent -and @($def.mobileCommandBarContent).
X "`t`t`t<xr:CheckState>0</xr:CheckState>" X "`t`t`t<xr:CheckState>0</xr:CheckState>"
# пустое значение → самозакрывающийся тег (зеркало платформы) # пустое значение → самозакрывающийся тег (зеркало платформы)
if ([string]::IsNullOrEmpty("$nm")) { X "`t`t`t<xr:Value xsi:type=`"xs:string`"/>" } if ([string]::IsNullOrEmpty("$nm")) { X "`t`t`t<xr:Value xsi:type=`"xs:string`"/>" }
else { X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-Xml "$nm")</xr:Value>" } else { X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-XmlText "$nm")</xr:Value>" }
X "`t`t</xr:Item>" X "`t`t</xr:Item>"
} }
X "`t</MobileDeviceCommandBarContent>" X "`t</MobileDeviceCommandBarContent>"
@@ -6622,7 +6666,7 @@ if (-not (Test-Path $outDir)) {
} }
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc) [System.IO.File]::WriteAllText($outPath, $xml.ToString().TrimEnd("`r", "`n"), $enc)
# --- 13b. Auto-register form in parent object XML --- # --- 13b. Auto-register form in parent object XML ---
@@ -6677,11 +6721,26 @@ if ($formsLeaf -eq 'Forms') {
$regSettings = New-Object System.Xml.XmlWriterSettings $regSettings = New-Object System.Xml.XmlWriterSettings
$regSettings.Encoding = $regEnc $regSettings.Encoding = $regEnc
$regSettings.Indent = $false $regSettings.Indent = $false
$regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create) $regSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
$regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings) # Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$regMem = New-Object System.IO.MemoryStream
$regWriter = [System.Xml.XmlWriter]::Create($regMem, $regSettings)
$objDoc.Save($regWriter) $objDoc.Save($regWriter)
$regWriter.Close() $regWriter.Flush(); $regWriter.Close()
$regStream.Close()
$regText = [System.Text.Encoding]::UTF8.GetString($regMem.ToArray())
$regMem.Close()
if ($regText.Length -gt 0 -and $regText[0] -eq [char]0xFEFF) { $regText = $regText.Substring(1) }
$regText = $regText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$regText = [regex]::Replace($regText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $objectXmlPath) -and ([System.IO.File]::ReadAllText($objectXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$regText = ($regText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($objectXmlPath, $regText, $regEnc)
Write-Host " Registered: <Form>$formName</Form> in $objectName.xml" Write-Host " Registered: <Form>$formName</Form> in $objectName.xml"
} }
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft) # form-decompile v0.148 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
param( param(
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft) # form-decompile v0.148 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
# #
@@ -13,6 +13,28 @@ import xml.etree.ElementTree as ET
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- 1. Namespaces --- # --- 1. Namespaces ---
NS_LF = "http://v8.1c.ru/8.3/xcf/logform" NS_LF = "http://v8.1c.ru/8.3/xcf/logform"
NS_V8 = "http://v8.1c.ru/8.1/data/core" NS_V8 = "http://v8.1c.ru/8.1/data/core"
@@ -3115,7 +3137,7 @@ def main():
parser = argparse.ArgumentParser(description='Decompile 1C managed Form.xml to JSON DSL', allow_abbrev=False) parser = argparse.ArgumentParser(description='Decompile 1C managed Form.xml to JSON DSL', allow_abbrev=False)
parser.add_argument('-FormPath', '-Path', dest='FormPath', type=str, required=True) parser.add_argument('-FormPath', '-Path', dest='FormPath', type=str, required=True)
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None) parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
args = parser.parse_args() args = ci_parse_args(parser)
form_path = args.FormPath form_path = args.FormPath
output_path = args.OutputPath output_path = args.OutputPath
+51 -12
View File
@@ -1,4 +1,4 @@
# form-edit v1.6 — Edit 1C managed form elements # form-edit v1.14 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -270,6 +270,12 @@ function X {
} }
function Esc-Xml { function Esc-Xml {
param([string]$s)
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
function Esc-XmlText {
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует — # Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной &quot;). &quot; платформа # пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной &quot;). &quot; платформа
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе. # принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
@@ -282,7 +288,7 @@ function Emit-MLText {
X "$indent<$tag>" X "$indent<$tag>"
X "$indent`t<v8:item>" X "$indent`t<v8:item>"
X "$indent`t`t<v8:lang>ru</v8:lang>" X "$indent`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>" X "$indent`t`t<v8:content>$(Esc-XmlText $text)</v8:content>"
X "$indent`t</v8:item>" X "$indent`t</v8:item>"
X "$indent</$tag>" X "$indent</$tag>"
} }
@@ -311,24 +317,49 @@ $script:formTypeSynonyms["бизнеспроцессссылка"] = "
$script:formTypeSynonyms["задачассылка"] = "TaskRef" $script:formTypeSynonyms["задачассылка"] = "TaskRef"
$script:formTypeSynonyms["определяемыйтип"] = "DefinedType" $script:formTypeSynonyms["определяемыйтип"] = "DefinedType"
# Алиас на локальный словарь: тело Resolve-TypeStr ниже — общая реализация,
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
$script:typeSynonyms = $script:formTypeSynonyms
function Resolve-TypeStr { function Resolve-TypeStr {
param([string]$typeStr) param([string]$typeStr)
if (-not $typeStr) { return $typeStr } if (-not $typeStr) { return $typeStr }
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
if ($typeStr.StartsWith('cfg:')) {
$typeStr = $typeStr.Substring(4)
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
}
# Параметризованные типы: Number(15,2), Строка(100)
if ($typeStr -match '^([^(]+)\((.+)\)$') { if ($typeStr -match '^([^(]+)\((.+)\)$') {
$base = $Matches[1].Trim(); $params = $Matches[2] $baseName = $Matches[1].Trim()
$r = $script:formTypeSynonyms[$base.ToLower()] $params = $Matches[2]
if ($r) { return "$r($params)" } $resolved = $script:typeSynonyms[$baseName.ToLower()]
if ($resolved) { return "$resolved($params)" }
return $typeStr return $typeStr
} }
# Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
if ($typeStr.Contains('.')) { if ($typeStr.Contains('.')) {
$i = $typeStr.IndexOf('.') $dotIdx = $typeStr.IndexOf('.')
$prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i) $prefix = $typeStr.Substring(0, $dotIdx)
$r = $script:formTypeSynonyms[$prefix.ToLower()] $suffix = $typeStr.Substring($dotIdx) # includes the dot
if ($r) { return "$r$suffix" } $resolved = $script:typeSynonyms[$prefix.ToLower()]
if ($resolved) { return "$resolved$suffix" }
return $typeStr return $typeStr
} }
$r = $script:formTypeSynonyms[$typeStr.ToLower()]
if ($r) { return $r } # Простое имя
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
if ($resolved) { return $resolved }
return $typeStr return $typeStr
} }
@@ -592,7 +623,7 @@ function Emit-Label {
X "$inner<Title formatted=`"$formatted`">" X "$inner<Title formatted=`"$formatted`">"
X "$inner`t<v8:item>" X "$inner`t<v8:item>"
X "$inner`t`t<v8:lang>ru</v8:lang>" X "$inner`t`t<v8:lang>ru</v8:lang>"
X "$inner`t`t<v8:content>$(Esc-Xml "$($el.title)")</v8:content>" X "$inner`t`t<v8:content>$(Esc-XmlText "$($el.title)")</v8:content>"
X "$inner`t</v8:item>" X "$inner`t</v8:item>"
X "$inner</Title>" X "$inner</Title>"
} }
@@ -1387,8 +1418,16 @@ if ($def.elementEvents -and $def.elementEvents.Count -gt 0) {
$content = $xmlDoc.OuterXml $content = $xmlDoc.OuterXml
# Ensure encoding declaration is uppercase UTF-8 # Ensure encoding declaration is uppercase UTF-8
$content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '<?xml version="1.0" encoding="UTF-8"?>' $content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '<?xml version="1.0" encoding="UTF-8"?>'
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$content = [regex]::Replace($content, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $resolvedFormPath) -and ([System.IO.File]::ReadAllText($resolvedFormPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$content = ($content -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc) [System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc)
# === 14. Summary === # === 14. Summary ===
+105 -16
View File
@@ -1,4 +1,4 @@
# form-edit v1.6 — Edit 1C managed form elements (Python port) # form-edit v1.14 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import json import json
@@ -11,6 +11,65 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================ # ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin # Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
@@ -192,7 +251,7 @@ def assert_edit_allowed(target_path, require):
parser = argparse.ArgumentParser(allow_abbrev=False) parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-FormPath", "-Path", required=True) parser.add_argument("-FormPath", "-Path", required=True)
parser.add_argument("-JsonPath", required=True) parser.add_argument("-JsonPath", required=True)
args = parser.parse_args() args = ci_parse_args(parser)
form_path = args.FormPath form_path = args.FormPath
json_path = args.JsonPath json_path = args.JsonPath
@@ -226,6 +285,11 @@ def local_name(node):
# ── helpers ────────────────────────────────────────────────── # ── helpers ──────────────────────────────────────────────────
def esc_xml(s): def esc_xml(s):
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def esc_xml_text(s):
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует """Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
(92142 сырых кавычки на корпус, ни одной &quot;); &quot; она принимает, но нормализует обратно.""" (92142 сырых кавычки на корпус, ни одной &quot;); &quot; она принимает, но нормализует обратно."""
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;') return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
@@ -254,7 +318,7 @@ root = tree.getroot()
# ── 2. Load JSON ──────────────────────────────────────────── # ── 2. Load JSON ────────────────────────────────────────────
with open(json_path, "r", encoding="utf-8-sig") as f: with open(json_path, "r", encoding="utf-8-sig") as f:
defn = json.load(f) defn = ci_json(json.load(f))
# ── 3. Form name + header ─────────────────────────────────── # ── 3. Form name + header ───────────────────────────────────
@@ -386,23 +450,48 @@ _FORM_TYPE_SYNONYMS = {
} }
# Алиас на локальный словарь: тело resolve_type_str ниже — общая реализация,
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
TYPE_SYNONYMS = _FORM_TYPE_SYNONYMS
def resolve_type_str(type_str): def resolve_type_str(type_str):
if not type_str: if not type_str:
return type_str return type_str
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
if type_str.startswith('cfg:'):
type_str = type_str[4:]
elif '.' in type_str and re.match(r'^d\d+p\d+:', type_str):
type_str = type_str[type_str.index(':') + 1:]
# Параметризованные типы: Number(15,2), Строка(100)
m = re.match(r'^([^(]+)\((.+)\)$', type_str) m = re.match(r'^([^(]+)\((.+)\)$', type_str)
if m: if m:
base, params = m.group(1).strip(), m.group(2) base_name = m.group(1).strip()
r = _FORM_TYPE_SYNONYMS.get(base.lower()) params = m.group(2)
return f"{r}({params})" if r else type_str resolved = TYPE_SYNONYMS.get(base_name.lower())
if resolved:
return f'{resolved}({params})'
return type_str
# Ссылочные типы: СправочникСсылка.Организации -> CatalogRef.Организации
if '.' in type_str: if '.' in type_str:
i = type_str.index('.') dot_idx = type_str.index('.')
prefix, suffix = type_str[:i], type_str[i:] prefix = type_str[:dot_idx]
r = _FORM_TYPE_SYNONYMS.get(prefix.lower()) suffix = type_str[dot_idx:] # includes the dot
return f"{r}{suffix}" if r else type_str resolved = TYPE_SYNONYMS.get(prefix.lower())
r = _FORM_TYPE_SYNONYMS.get(type_str.lower()) if resolved:
return r if r else type_str return f'{resolved}{suffix}'
return type_str
# Простое имя
resolved = TYPE_SYNONYMS.get(type_str.lower())
if resolved:
return resolved
return type_str
def emit_type(type_str, indent): def emit_type(type_str, indent):
if not type_str: if not type_str:
X(f"{indent}<Type/>") X(f"{indent}<Type/>")
@@ -496,7 +585,7 @@ def emit_mltext(tag, text, indent):
X(f"{indent}<{tag}>") X(f"{indent}<{tag}>")
X(f"{indent}\t<v8:item>") X(f"{indent}\t<v8:item>")
X(f"{indent}\t\t<v8:lang>ru</v8:lang>") X(f"{indent}\t\t<v8:lang>ru</v8:lang>")
X(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>") X(f"{indent}\t\t<v8:content>{esc_xml_text(text)}</v8:content>")
X(f"{indent}\t</v8:item>") X(f"{indent}\t</v8:item>")
X(f"{indent}</{tag}>") X(f"{indent}</{tag}>")
@@ -724,7 +813,7 @@ def emit_label(el, name, _id, indent):
X(f'{inner}<Title formatted="{formatted}">') X(f'{inner}<Title formatted="{formatted}">')
X(f"{inner}\t<v8:item>") X(f"{inner}\t<v8:item>")
X(f"{inner}\t\t<v8:lang>ru</v8:lang>") X(f"{inner}\t\t<v8:lang>ru</v8:lang>")
X(f"{inner}\t\t<v8:content>{esc_xml(str(el['title']))}</v8:content>") X(f"{inner}\t\t<v8:content>{esc_xml_text(str(el['title']))}</v8:content>")
X(f"{inner}\t</v8:item>") X(f"{inner}\t</v8:item>")
X(f"{inner}</Title>") X(f"{inner}</Title>")
emit_common_flags(el, inner) emit_common_flags(el, inner)
@@ -1,4 +1,4 @@
# form-info v1.5 — Analyze 1C managed form structure # form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
+27 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-info v1.5 — Analyze 1C managed form structure # form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -8,6 +8,28 @@ import re
import sys import sys
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Namespace map --- # --- Namespace map ---
NSMAP = { NSMAP = {
@@ -353,7 +375,7 @@ def get_support_status_for_path(target_path):
except Exception: except Exception:
pass pass
return None return None
def is_external_root(xml_path): def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path): if not os.path.isfile(xml_path):
return False return False
try: try:
@@ -367,14 +389,14 @@ def get_support_status_for_path(target_path):
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
elem_uuid = root_uuid(rp) elem_uuid = root_uuid(rp)
if is_external_root(rp): if _sg_is_external_root(rp):
return None return None
bin_path = None bin_path = None
d = os.path.dirname(rp) d = os.path.dirname(rp)
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if is_external_root(d + ".xml"): if _sg_is_external_root(d + ".xml"):
return None return None
if not elem_uuid: if not elem_uuid:
elem_uuid = root_uuid(d + ".xml") elem_uuid = root_uuid(d + ".xml")
@@ -433,7 +455,7 @@ def main():
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show") parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination") parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all") parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all")
args = parser.parse_args() args = ci_parse_args(parser)
form_path = args.FormPath form_path = args.FormPath
limit = args.Limit limit = args.Limit
@@ -1,4 +1,4 @@
# form-remove v1.4 — Remove form from 1C object # form-remove v1.9 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -64,6 +64,10 @@ foreach ($node in $formNodes) {
$parent.RemoveChild($prev) | Out-Null $parent.RemoveChild($prev) | Out-Null
} }
$parent.RemoveChild($node) | Out-Null $parent.RemoveChild($node) | Out-Null
# Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару
# <ChildObjects>\n\t\t</ChildObjects>. Платформа пишет только <ChildObjects/>
# (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0).
if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true }
break break
} }
} }
@@ -74,7 +78,9 @@ foreach ($node in $formNodes) {
$formRefRe = "Form\.$([regex]::Escape($FormName))$" $formRefRe = "Form\.$([regex]::Escape($FormName))$"
foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) { foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) {
if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) { if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) {
$node.InnerText = "" # IsEmpty, а не InnerText="": пустая строка сериализуется парой <Tag></Tag>, а
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
$node.IsEmpty = $true
} }
} }
@@ -83,11 +89,26 @@ $encBom = New-Object System.Text.UTF8Encoding($true)
$settings = New-Object System.Xml.XmlWriterSettings $settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom $settings.Encoding = $encBom
$settings.Indent = $false $settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create) # Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$writer = [System.Xml.XmlWriter]::Create($stream, $settings) $memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$xmlDoc.Save($writer) $xmlDoc.Save($writer)
$writer.Close() $writer.Flush(); $writer.Close()
$stream.Close()
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $rootXmlFull.Path) -and ([System.IO.File]::ReadAllText($rootXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($rootXmlFull.Path, $xmlText, $encBom)
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath" Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# remove-form v1.4 — Remove form from 1C object # form-remove v1.9 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -10,6 +10,28 @@ import sys
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
@@ -30,21 +52,22 @@ def _detect_xml_style(path):
def _finalize_xml_bytes(xml_bytes, style): def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None).""" """Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
enc_decl = style["enc"] if style else "utf-8" выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace( xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>", b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>') b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах) # Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"") xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n")) .replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть) # Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else True want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n") xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl: if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение) # EOL — как в оригинале (новый файл → CRLF, канон #57)
if style and style["crlf"]: if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n") xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes return xml_bytes
@@ -67,7 +90,7 @@ def main():
parser.add_argument("-ObjectName", "-ProcessorName", required=True) parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-FormName", required=True) parser.add_argument("-FormName", required=True)
parser.add_argument("-SrcDir", default="src") parser.add_argument("-SrcDir", default="src")
args = parser.parse_args() args = ci_parse_args(parser)
object_name = args.ObjectName object_name = args.ObjectName
form_name = args.FormName form_name = args.FormName
@@ -119,6 +142,10 @@ def main():
if parent.text and parent.text.strip() == "": if parent.text and parent.text.strip() == "":
parent.text = "" parent.text = ""
parent.remove(node) parent.remove(node)
# Опустевший контейнер: text="" сериализуется парой <ChildObjects></ChildObjects>,
# а нужен <ChildObjects/> — PS-порт через DOM даёт именно его.
if len(parent) == 0 and not (parent.text or "").strip():
parent.text = None
break break
# Clear any Default*/Auxiliary* form slot that pointed to the removed form # Clear any Default*/Auxiliary* form slot that pointed to the removed form
@@ -129,7 +156,9 @@ def main():
if not isinstance(el.tag, str): if not isinstance(el.tag, str):
continue continue
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text): if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
el.text = "" # text=None, а не "": пустая строка сериализуется парой <Tag></Tag>, а
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
el.text = None
# Save with BOM # Save with BOM
save_xml_with_bom(tree, root_xml_full) save_xml_with_bom(tree, root_xml_full)
@@ -1,4 +1,4 @@
# form-validate v1.9 — Validate 1C managed form # form-validate v1.10 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-validate v1.9 — Validate 1C managed form # form-validate v1.10 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -8,6 +8,28 @@ import re
import sys import sys
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
F_NS = "http://v8.1c.ru/8.3/xcf/logform" F_NS = "http://v8.1c.ru/8.3/xcf/logform"
V8_NS = "http://v8.1c.ru/8.1/data/core" V8_NS = "http://v8.1c.ru/8.1/data/core"
@@ -60,7 +82,7 @@ def main():
parser.add_argument("-FormPath", "-Path", required=True) parser.add_argument("-FormPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true") parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30) parser.add_argument("-MaxErrors", type=int, default=30)
args = parser.parse_args() args = ci_parse_args(parser)
form_path = args.FormPath form_path = args.FormPath
detailed = args.Detailed detailed = args.Detailed
+44 -8
View File
@@ -1,4 +1,4 @@
# help-add v1.9 — Add built-in help to 1C object # help-add v1.19 — Add built-in help to 1C object (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -149,10 +149,20 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
function Detect-FormatVersion([string]$dir) { function Detect-FormatVersion([string]$dir) {
$d = $dir $d = $dir
while ($d) { while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml" $cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) { if (Test-Path $cfgPath) {
$content = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8) $cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
$head = $content.Substring(0, [Math]::Min(2000, $content.Length)) # Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] } if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
} }
$parent = Split-Path $d -Parent $parent = Split-Path $d -Parent
@@ -195,7 +205,18 @@ $helpXml = @"
</Help> </Help>
"@ "@
[System.IO.File]::WriteAllText($helpXmlPath, $helpXml, $encBom) # XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
#
# HTML-страница сюда НЕ идёт — платформа хранит её с LF.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $helpXmlPath $helpXml $encBom
# --- 2. Help/<lang>.html --- # --- 2. Help/<lang>.html ---
@@ -255,11 +276,26 @@ if (Test-Path $formsDir) {
$settings = New-Object System.Xml.XmlWriterSettings $settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom $settings.Encoding = $encBom
$settings.Indent = $false $settings.Indent = $false
$stream = New-Object System.IO.FileStream($formMeta.FullName, [System.IO.FileMode]::Create) $settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$writer = [System.Xml.XmlWriter]::Create($stream, $settings) # Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$xmlDoc.Save($writer) $xmlDoc.Save($writer)
$writer.Close() $writer.Flush(); $writer.Close()
$stream.Close()
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $formMeta.FullName) -and ([System.IO.File]::ReadAllText($formMeta.FullName) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($formMeta.FullName, $xmlText, $encBom)
Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)" Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)"
} }
+61 -14
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# add-help v1.9 — Add built-in help to 1C object # help-add v1.19 — Add built-in help to 1C object (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -10,6 +10,28 @@ import sys
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
@@ -191,6 +213,16 @@ def assert_edit_allowed(target_path, require):
def detect_format_version(d): def detect_format_version(d):
while d: while d:
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
ext_path = d + ".xml"
if os.path.isfile(ext_path):
with open(ext_path, "r", encoding="utf-8-sig") as f:
ext_head = f.read(2000)
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
if m:
return m.group(1)
cfg_path = os.path.join(d, "Configuration.xml") cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path): if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f: with open(cfg_path, "r", encoding="utf-8-sig") as f:
@@ -222,21 +254,22 @@ def _detect_xml_style(path):
def _finalize_xml_bytes(xml_bytes, style): def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None).""" """Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
enc_decl = style["enc"] if style else "utf-8" выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace( xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>", b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>') b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах) # Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"") xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n")) .replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть) # Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else True want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n") xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl: if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение) # EOL — как в оригинале (новый файл → CRLF, канон #57)
if style and style["crlf"]: if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n") xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes return xml_bytes
@@ -252,10 +285,24 @@ def save_xml_with_bom(tree, path):
f.write(xml_bytes) f.write(xml_bytes)
def write_text_with_bom(path, text): def write_utf8_bom(path, content):
"""Write text to file with UTF-8 BOM.""" # newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
with open(path, "w", encoding="utf-8-sig") as f: # и LF на macOS, то есть вывод навыка зависел бы от ОС.
f.write(text) with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
HTML-страница сюда НЕ идёт платформа хранит её с LF.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def main(): def main():
@@ -265,7 +312,7 @@ def main():
parser.add_argument("-ObjectName", required=True) parser.add_argument("-ObjectName", required=True)
parser.add_argument("-Lang", default="ru") parser.add_argument("-Lang", default="ru")
parser.add_argument("-SrcDir", default="src") parser.add_argument("-SrcDir", default="src")
args = parser.parse_args() args = ci_parse_args(parser)
object_name = args.ObjectName object_name = args.ObjectName
lang = args.Lang lang = args.Lang
@@ -301,7 +348,7 @@ def main():
'</Help>' '</Help>'
) )
write_text_with_bom(help_xml_path, help_xml) write_xml_file(help_xml_path, help_xml)
# --- 2. Help/<lang>.html --- # --- 2. Help/<lang>.html ---
@@ -324,7 +371,7 @@ def main():
'</html>' '</html>'
) )
write_text_with_bom(help_html_path, help_html) write_utf8_bom(help_html_path, help_html)
# --- 3. Check IncludeHelpInContents in form metadata --- # --- 3. Check IncludeHelpInContents in form metadata ---
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# img-grid v1.1 — Overlay numbered grid on image # img-grid v1.2 — Overlay numbered grid on image
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Overlay a numbered grid on an image to help determine column/row proportions. """Overlay a numbered grid on an image to help determine column/row proportions.
@@ -16,6 +16,28 @@ import os
import sys import sys
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
MARGIN_TOP = 20 MARGIN_TOP = 20
MARGIN_LEFT = 24 MARGIN_LEFT = 24
@@ -30,7 +52,7 @@ def main():
parser.add_argument("-r", "--rows", type=int, default=0, parser.add_argument("-r", "--rows", type=int, default=0,
help="Number of horizontal divisions (0 = auto, match cell aspect ratio)") help="Number of horizontal divisions (0 = auto, match cell aspect ratio)")
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)") parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
args = parser.parse_args() args = ci_parse_args(parser)
if args.cols <= 0: if args.cols <= 0:
parser.error("--cols must be greater than 0") parser.error("--cols must be greater than 0")
@@ -1,4 +1,4 @@
# interface-edit v1.9 — Edit 1C CommandInterface.xml # interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath, [Parameter(Mandatory)][Alias('Path')][string]$CIPath,
@@ -162,6 +162,14 @@ Assert-EditAllowed $CIPath 'editable'
function Detect-FormatVersion([string]$dir) { function Detect-FormatVersion([string]$dir) {
$d = $dir $d = $dir
while ($d) { while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml" $cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) { if (Test-Path $cfgPath) {
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8) $cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
@@ -202,7 +210,12 @@ if (-not (Test-Path $CIPath)) {
</CommandInterface> </CommandInterface>
"@ "@
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($CIPath, $emptyCI, $utf8Bom) # Файл СОЗДАЁМ — пишем канон выгрузки: CRLF, без перевода строки в конце.
# (Правка существующего файла, наоборот, наследует его стиль — это делает
# основной путь сохранения ниже.) Нормализация нужна потому, что here-string
# берёт переводы строк из самого .ps1, а он в репозитории хранится с LF.
$emptyCI = ($emptyCI -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($CIPath, $emptyCI.TrimEnd("`r", "`n"), $utf8Bom)
Write-Host "[INFO] Created new CommandInterface.xml: $CIPath" Write-Host "[INFO] Created new CommandInterface.xml: $CIPath"
} else { } else {
Write-Error "File not found: $CIPath (use -CreateIfMissing to create)" Write-Error "File not found: $CIPath (use -CreateIfMissing to create)"
@@ -383,6 +396,10 @@ $script:typeNormMap = @{
"ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal" "ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal"
"ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand" "ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand"
"ОбщаяФорма"="CommonForm"; "Подсистема"="Subsystem" "ОбщаяФорма"="CommonForm"; "Подсистема"="Subsystem"
"РегистрРасчёта"="CalculationRegister"; "РегистрРасчета"="CalculationRegister"
"ПланВидовРасчёта"="ChartOfCalculationTypes"; "ПланВидовРасчета"="ChartOfCalculationTypes"
"Роль"="Role"; "ОбщийМакет"="CommonTemplate"; "ЭлементСтиля"="StyleItem"
"ОбщийРеквизит"="CommonAttribute"; "ГруппаКоманд"="CommandGroup"
# Russian plural # Russian plural
"Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum" "Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum"
"Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report"; "Обработки"="DataProcessor" "Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report"; "Обработки"="DataProcessor"
@@ -392,6 +409,10 @@ $script:typeNormMap = @{
"БизнесПроцессы"="BusinessProcess"; "Задачи"="Task" "БизнесПроцессы"="BusinessProcess"; "Задачи"="Task"
"ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal" "ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal"
"Подсистемы"="Subsystem" "Подсистемы"="Subsystem"
"РегистрыРасчёта"="CalculationRegister"; "РегистрыРасчета"="CalculationRegister"
"ПланыВидовРасчёта"="ChartOfCalculationTypes"; "ПланыВидовРасчета"="ChartOfCalculationTypes"
"Роли"="Role"; "ОбщиеМакеты"="CommonTemplate"; "ЭлементыСтиля"="StyleItem"
"ОбщиеРеквизиты"="CommonAttribute"; "ГруппыКоманд"="CommandGroup"
} }
function Normalize-CmdName([string]$name) { function Normalize-CmdName([string]$name) {
@@ -674,8 +695,16 @@ $memStream.Close()
$text = [System.Text.Encoding]::UTF8.GetString($bytes) $text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') $text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $utf8Bom = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) [System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
Info "Saved: $resolvedPath" Info "Saved: $resolvedPath"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# interface-edit v1.9 — Edit 1C CommandInterface.xml # interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -10,6 +10,65 @@ import subprocess
import sys import sys
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================ # ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
@@ -189,6 +248,16 @@ def assert_edit_allowed(target_path, require):
def detect_format_version(d): def detect_format_version(d):
while d: while d:
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
ext_path = d + ".xml"
if os.path.isfile(ext_path):
with open(ext_path, "r", encoding="utf-8-sig") as f:
ext_head = f.read(2000)
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
if m:
return m.group(1)
cfg_path = os.path.join(d, "Configuration.xml") cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path): if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f: with open(cfg_path, "r", encoding="utf-8-sig") as f:
@@ -282,7 +351,7 @@ def import_ci_fragment(xml_string):
def parse_value_list(val): def parse_value_list(val):
val = val.strip() val = val.strip()
if val.startswith("["): if val.startswith("["):
arr = json.loads(val) arr = ci_json(json.loads(val))
return [str(item) for item in arr] return [str(item) for item in arr]
return [val] return [val]
@@ -304,21 +373,22 @@ def _detect_xml_style(path):
def _finalize_xml_bytes(xml_bytes, style): def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None).""" """Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
enc_decl = style["enc"] if style else "utf-8" выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace( xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>", b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>') b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах) # Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"") xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n")) .replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть) # Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else True want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n") xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl: if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение) # EOL — как в оригинале (новый файл → CRLF, канон #57)
if style and style["crlf"]: if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n") xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes return xml_bytes
@@ -357,6 +427,10 @@ TYPE_NORM_MAP = {
'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal', 'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal',
'ОбщийМодуль': 'CommonModule', 'ОбщаяКоманда': 'CommonCommand', 'ОбщийМодуль': 'CommonModule', 'ОбщаяКоманда': 'CommonCommand',
'ОбщаяФорма': 'CommonForm', 'Подсистема': 'Subsystem', 'ОбщаяФорма': 'CommonForm', 'Подсистема': 'Subsystem',
'РегистрРасчёта': 'CalculationRegister', 'РегистрРасчета': 'CalculationRegister',
'ПланВидовРасчёта': 'ChartOfCalculationTypes', 'ПланВидовРасчета': 'ChartOfCalculationTypes',
'Роль': 'Role', 'ОбщийМакет': 'CommonTemplate', 'ЭлементСтиля': 'StyleItem',
'ОбщийРеквизит': 'CommonAttribute', 'ГруппаКоманд': 'CommandGroup',
# Russian plural # Russian plural
'Справочники': 'Catalog', 'Документы': 'Document', 'Перечисления': 'Enum', 'Справочники': 'Catalog', 'Документы': 'Document', 'Перечисления': 'Enum',
'Константы': 'Constant', 'Отчёты': 'Report', 'Отчеты': 'Report', 'Обработки': 'DataProcessor', 'Константы': 'Constant', 'Отчёты': 'Report', 'Отчеты': 'Report', 'Обработки': 'DataProcessor',
@@ -366,6 +440,10 @@ TYPE_NORM_MAP = {
'БизнесПроцессы': 'BusinessProcess', 'Задачи': 'Task', 'БизнесПроцессы': 'BusinessProcess', 'Задачи': 'Task',
'ПланыОбмена': 'ExchangePlan', 'ЖурналыДокументов': 'DocumentJournal', 'ПланыОбмена': 'ExchangePlan', 'ЖурналыДокументов': 'DocumentJournal',
'Подсистемы': 'Subsystem', 'Подсистемы': 'Subsystem',
'РегистрыРасчёта': 'CalculationRegister', 'РегистрыРасчета': 'CalculationRegister',
'ПланыВидовРасчёта': 'ChartOfCalculationTypes', 'ПланыВидовРасчета': 'ChartOfCalculationTypes',
'Роли': 'Role', 'ОбщиеМакеты': 'CommonTemplate', 'ЭлементыСтиля': 'StyleItem',
'ОбщиеРеквизиты': 'CommonAttribute', 'ГруппыКоманд': 'CommandGroup',
} }
@@ -401,7 +479,7 @@ def main():
parser.add_argument("-Value", default=None) parser.add_argument("-Value", default=None)
parser.add_argument("-CreateIfMissing", action="store_true") parser.add_argument("-CreateIfMissing", action="store_true")
parser.add_argument("-NoValidate", action="store_true") parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args() args = ci_parse_args(parser)
# --- Mode validation --- # --- Mode validation ---
if args.DefinitionFile and args.Operation: if args.DefinitionFile and args.Operation:
@@ -438,7 +516,12 @@ def main():
f'\tversion="{format_version}">\n' f'\tversion="{format_version}">\n'
f'</CommandInterface>' f'</CommandInterface>'
) )
with open(ci_path, "w", encoding="utf-8-sig") as fh: # Файл СОЗДАЁМ — пишем канон выгрузки: CRLF в разделителях. (Правка
# существующего файла, наоборот, наследует его стиль — это делает
# save_xml_bom через _detect_xml_style.) newline="" обязателен: без него
# текстовый режим дал бы CRLF на Windows и LF на macOS.
empty_ci = empty_ci.replace("\r\n", "\n").replace("\n", "\r\n").rstrip("\r\n")
with open(ci_path, "w", encoding="utf-8-sig", newline="") as fh:
fh.write(empty_ci) fh.write(empty_ci)
print(f"[INFO] Created new CommandInterface.xml: {ci_path}") print(f"[INFO] Created new CommandInterface.xml: {ci_path}")
else: else:
@@ -564,7 +647,7 @@ def main():
def do_place(json_val): def do_place(json_val):
nonlocal add_count, modify_count nonlocal add_count, modify_count
defn = json_val if isinstance(json_val, dict) else json.loads(json_val) defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
cmd_name = normalize_cmd_name(str(defn["command"])) cmd_name = normalize_cmd_name(str(defn["command"]))
group_name = str(defn["group"]) group_name = str(defn["group"])
if not cmd_name or not group_name: if not cmd_name or not group_name:
@@ -592,7 +675,7 @@ def main():
def do_order(json_val): def do_order(json_val):
nonlocal add_count, remove_count nonlocal add_count, remove_count
defn = json_val if isinstance(json_val, dict) else json.loads(json_val) defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
group_name = str(defn["group"]) group_name = str(defn["group"])
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]] commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
if not group_name or not commands: if not group_name or not commands:
@@ -626,7 +709,7 @@ def main():
def do_subsystem_order(json_val): def do_subsystem_order(json_val):
nonlocal add_count, remove_count nonlocal add_count, remove_count
parsed = json_val if isinstance(json_val, list) else json.loads(json_val) parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
subsystems = [str(s) for s in parsed] subsystems = [str(s) for s in parsed]
if not subsystems: if not subsystems:
print("subsystem-order requires array of subsystem paths", file=sys.stderr) print("subsystem-order requires array of subsystem paths", file=sys.stderr)
@@ -651,7 +734,7 @@ def main():
def do_group_order(json_val): def do_group_order(json_val):
nonlocal add_count, remove_count nonlocal add_count, remove_count
parsed = json_val if isinstance(json_val, list) else json.loads(json_val) parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
groups = [str(g) for g in parsed] groups = [str(g) for g in parsed]
if not groups: if not groups:
print("group-order requires array of group names", file=sys.stderr) print("group-order requires array of group names", file=sys.stderr)
@@ -681,7 +764,7 @@ def main():
if not os.path.isabs(def_file): if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file) def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh: with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = json.loads(fh.read()) ops = ci_json(json.loads(fh.read()))
if isinstance(ops, list): if isinstance(ops, list):
operations = ops operations = ops
else: else:
@@ -691,19 +774,21 @@ def main():
for op in operations: for op in operations:
op_name = op.get("operation", args.Operation or "") op_name = op.get("operation", args.Operation or "")
# PS сравнивает имя операции через switch, а он регистронезависим.
op_key = str(op_name).lower()
op_value = op.get("value", args.Value or "") op_value = op.get("value", args.Value or "")
if op_name == "hide": if op_key == "hide":
do_hide(parse_value_list(op_value)) do_hide(parse_value_list(op_value))
elif op_name == "show": elif op_key == "show":
do_show(parse_value_list(op_value)) do_show(parse_value_list(op_value))
elif op_name == "place": elif op_key == "place":
do_place(op_value) do_place(op_value)
elif op_name == "order": elif op_key == "order":
do_order(op_value) do_order(op_value)
elif op_name == "subsystem-order": elif op_key == "subsystem-order":
do_subsystem_order(op_value) do_subsystem_order(op_value)
elif op_name == "group-order": elif op_key == "group-order":
do_group_order(op_value) do_group_order(op_value)
else: else:
print(f"Unknown operation: {op_name}", file=sys.stderr) print(f"Unknown operation: {op_name}", file=sys.stderr)
@@ -1,4 +1,4 @@
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure # interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath, [Parameter(Mandatory)][Alias('Path')][string]$CIPath,
@@ -51,16 +51,21 @@ $script:output = New-Object System.Text.StringBuilder 8192
$script:allCommandNames = @() $script:allCommandNames = @()
function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null } function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null }
function Report-OK([string]$msg) { function Report-OK {
param([string]$msg)
$script:okCount++ $script:okCount++
if ($Detailed) { Out-Line "[OK] $msg" } if ($Detailed) { Out-Line "[OK] $msg" }
} }
function Report-Error([string]$msg) { function Report-Error {
param([string]$msg)
$script:errors++ $script:errors++
Out-Line "[ERROR] $msg" Out-Line "[ERROR] $msg"
if ($script:errors -ge $MaxErrors) { $script:stopped = $true } if ($script:errors -ge $MaxErrors) {
$script:stopped = $true
}
} }
function Report-Warn([string]$msg) { function Report-Warn {
param([string]$msg)
$script:warnings++ $script:warnings++
Out-Line "[WARN] $msg" Out-Line "[WARN] $msg"
} }
@@ -1,10 +1,32 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure # interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates CommandInterface.xml sections, command references, order, duplicates.""" """Validates CommandInterface.xml sections, command references, order, duplicates."""
import sys, os, argparse, re import sys, os, argparse, re
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NS_CI = 'http://v8.1c.ru/8.3/xcf/extrnprops' NS_CI = 'http://v8.1c.ru/8.3/xcf/extrnprops'
NS_XR = 'http://v8.1c.ru/8.3/xcf/readable' NS_XR = 'http://v8.1c.ru/8.3/xcf/readable'
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance' NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
@@ -83,7 +105,7 @@ def main():
parser.add_argument('-Detailed', action='store_true') parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30) parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='') parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args() args = ci_parse_args(parser)
ci_path = args.CIPath ci_path = args.CIPath
detailed = args.Detailed detailed = args.Detailed
@@ -1,4 +1,4 @@
# meta-compile v1.76 — Compile 1C metadata object from JSON # meta-compile v1.94 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -161,7 +161,10 @@ if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq '
$idx = 0 $idx = 0
foreach ($item in $def) { foreach ($item in $def) {
$idx++ $idx++
$tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx.json" # Имя с GUID, а не "batch-$idx": фиксированное имя в общем %TEMP% сталкивало
# два параллельных запуска навыка на одной машине — Set-Content падал с
# «file is being used by another process». py-порт уже брал mkstemp.
$tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx-$([guid]::NewGuid().ToString('N')).json"
try { try {
$item | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpJson $item | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpJson
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$PSCommandPath`" -JsonPath `"$tmpJson`" -OutputDir `"$OutputDir`"" -NoNewWindow -Wait -PassThru $proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$PSCommandPath`" -JsonPath `"$tmpJson`" -OutputDir `"$OutputDir`"" -NoNewWindow -Wait -PassThru
@@ -405,6 +408,11 @@ $validTypes = @("Catalog","Document","Enum","Constant","InformationRegister","Ac
"Sequence","FilterCriterion","DocumentNumerator","SettingsStorage","CommonForm", "Sequence","FilterCriterion","DocumentNumerator","SettingsStorage","CommonForm",
"SessionParameter","CommonCommand","CommandGroup","CommonAttribute","FunctionalOptionsParameter","WSReference", "SessionParameter","CommonCommand","CommandGroup","CommonAttribute","FunctionalOptionsParameter","WSReference",
"CommonPicture","CommonTemplate") "CommonPicture","CommonTemplate")
# -notin регистронезависим, поэтому "catalog" проходил проверку и дальше шёл в ИМЯ ТЕГА и в
# Configuration.xml как есть — выгрузка получалась с <catalog>, которую платформа не принимает.
# Прощаем регистр, но приводим к канону списка.
$canonType = $validTypes | Where-Object { $_ -eq $objType } | Select-Object -First 1
if ($canonType) { $objType = $canonType }
if ($objType -notin $validTypes) { if ($objType -notin $validTypes) {
Write-Error "Unsupported type: $objType. Valid: $($validTypes -join ', ')" Write-Error "Unsupported type: $objType. Valid: $($validTypes -join ', ')"
exit 1 exit 1
@@ -544,6 +552,10 @@ $script:typeNamespaceMap = @{
} }
# Типы current-config пространства (cfg:, объявлено в корне): объектные (CatalogObject.X/DataProcessorObject.X/…) # Типы current-config пространства (cfg:, объявлено в корне): объектные (CatalogObject.X/DataProcessorObject.X/…)
# и голые (ConstantsSet/ReportBuilder). Ссылочные (*Ref.X/DefinedType.X) идут ОТДЕЛЬНО через локальный d5p1 (§memory). # и голые (ConstantsSet/ReportBuilder). Ссылочные (*Ref.X/DefinedType.X) идут ОТДЕЛЬНО через локальный d5p1 (§memory).
# Префикс current-config для ссылочных типов. 'cfg' — для файлов, чья шапка его объявляет
# (объектный XML, Ext/Form.xml общей формы). $null на время сборки Ext/Predefined.xml, чья
# шапка его НЕ объявляет: там и платформа уходит на локальное объявление.
$script:cfgPrefix = 'cfg'
$script:cfgBareTypes = @("ConstantsSet", "ReportBuilder", "FilterCriterion") $script:cfgBareTypes = @("ConstantsSet", "ReportBuilder", "FilterCriterion")
$script:cfgObjectKinds = @("Catalog","Document","Enum","ChartOfAccounts","ChartOfCharacteristicTypes", $script:cfgObjectKinds = @("Catalog","Document","Enum","ChartOfAccounts","ChartOfCharacteristicTypes",
"ChartOfCalculationTypes","ExchangePlan","BusinessProcess","Task","InformationRegister","AccumulationRegister", "ChartOfCalculationTypes","ExchangePlan","BusinessProcess","Task","InformationRegister","AccumulationRegister",
@@ -574,7 +586,20 @@ function Resolve-TypeStr {
param([string]$typeStr) param([string]$typeStr)
if (-not $typeStr) { return $typeStr } if (-not $typeStr) { return $typeStr }
# Check for parameterized types: Number(15,2), Строка(100), etc. # Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
if ($typeStr.StartsWith('cfg:')) {
$typeStr = $typeStr.Substring(4)
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
}
# Параметризованные типы: Number(15,2), Строка(100)
if ($typeStr -match '^([^(]+)\((.+)\)$') { if ($typeStr -match '^([^(]+)\((.+)\)$') {
$baseName = $Matches[1].Trim() $baseName = $Matches[1].Trim()
$params = $Matches[2] $params = $Matches[2]
@@ -583,7 +608,7 @@ function Resolve-TypeStr {
return $typeStr return $typeStr
} }
# Check for reference types: СправочникСсылка.Организации → CatalogRef.Организации # Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
if ($typeStr.Contains('.')) { if ($typeStr.Contains('.')) {
$dotIdx = $typeStr.IndexOf('.') $dotIdx = $typeStr.IndexOf('.')
$prefix = $typeStr.Substring(0, $dotIdx) $prefix = $typeStr.Substring(0, $dotIdx)
@@ -593,10 +618,9 @@ function Resolve-TypeStr {
return $typeStr return $typeStr
} }
# Simple name lookup # Простое имя
$resolved = $script:typeSynonyms[$typeStr.ToLower()] $resolved = $script:typeSynonyms[$typeStr.ToLower()]
if ($resolved) { return $resolved } if ($resolved) { return $resolved }
return $typeStr return $typeStr
} }
@@ -605,10 +629,44 @@ function Emit-TypeContent {
if (-not $typeStr) { return } if (-not $typeStr) { return }
# Composite type: "Type1 + Type2 + Type3" # Composite type: "Type1 + Type2 + Type3"
# Платформа пишет сначала ВСЕ <v8:Type>/<v8:TypeSet>, и только потом блоки
# квалификаторов — а рекурсия ниже печатала бы каждую часть целиком (тип вместе со
# своими квалификаторами). На одиночном типе оба порядка совпадают, поэтому
# расхождение вылезало только на составном.
# Порядок самих блоков квалификаторов тоже канонический и НЕ зеркалит порядок типов:
# Number, String, Date (корпус acc+erp, контрпримеров нет — при типах
# boolean,string,dateTime,decimal квалификаторы идут Number,String,Date).
# Порядок типов при этом сохраняем как в DSL: он и есть порядок источника.
if ($typeStr.Contains(' + ')) { if ($typeStr.Contains(' + ')) {
$parts = $typeStr -split '\s*\+\s*' $parts = $typeStr -split '\s*\+\s*'
$typeLines = New-Object System.Collections.ArrayList
$qualBlocks = @{} # 'Number'|'String'|'Date' → строки блока
foreach ($part in $parts) { foreach ($part in $parts) {
# X пишет в StringBuilder, поэтому «перехват» — это запомнить длину, вызвать
# эмиттер и откатить добавленное. В py-порту X добавляет в список, и там тот
# же алгоритм выражен срезом — различие рантаймов, не логики.
$before = $script:xml.Length
Emit-TypeContent $indent $part.Trim() Emit-TypeContent $indent $part.Trim()
$chunk = $script:xml.ToString($before, $script:xml.Length - $before)
[void]$script:xml.Remove($before, $script:xml.Length - $before)
$curQual = $null
foreach ($line in ($chunk -split "`r?`n")) {
if ($line -eq '') { continue }
if ($line -match '<v8:(String|Number|Date)Qualifiers>') {
$curQual = $Matches[1]
$qualBlocks[$curQual] = New-Object System.Collections.ArrayList
}
if ($curQual) {
[void]$qualBlocks[$curQual].Add($line)
if ($line -match '</v8:(String|Number|Date)Qualifiers>') { $curQual = $null }
} else {
[void]$typeLines.Add($line)
}
}
}
foreach ($line in $typeLines) { X $line }
foreach ($q in @('Number', 'String', 'Date')) {
if ($qualBlocks.ContainsKey($q)) { foreach ($line in $qualBlocks[$q]) { X $line } }
} }
return return
} }
@@ -723,9 +781,22 @@ function Emit-TypeContent {
return return
} }
# Reference types — use local xmlns declaration for 1C compatibility # Ссылочные типы — корневой cfg:, как пишет платформа. Раньше здесь объявлялся
# ЛОКАЛЬНЫЙ xmlns:d5p1 на тот же URI, что уже объявлен в шапке ($script:xmlnsDecl):
# формально эквивалентно (значим URI, не префикс) и платформой принималось, но
# первый же цикл «загрузить в базу → выгрузить» переписывал каждый ссылочный тип
# в cfg: — то есть давал diff-шум на ровном месте. Форма пришла из СКД, где cfg:
# действительно не работает; в метаданных такого ограничения нет.
# NB: локальная xmlns остаётся законной для ЧУЖИХ пространств — см. $script:typeNamespaceMap.
# $script:cfgPrefix = $null означает «пишем файл, корень которого cfg НЕ объявляет»
# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$') { if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$') {
X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$typeStr</v8:Type>" if ($script:cfgPrefix) {
X "$indent<v8:Type>$($script:cfgPrefix):$typeStr</v8:Type>"
} else {
X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$typeStr</v8:Type>"
}
return return
} }
@@ -1245,15 +1316,21 @@ $script:standardAttributesByType = @{
"Document" = @("Posted","Ref","DeletionMark","Date","Number") "Document" = @("Posted","Ref","DeletionMark","Date","Number")
"Enum" = @("Order","Ref") "Enum" = @("Order","Ref")
"InformationRegister" = @("Active","LineNumber","Recorder","Period") "InformationRegister" = @("Active","LineNumber","Recorder","Period")
"AccumulationRegister" = @("Active","LineNumber","Recorder","Period") "AccumulationRegister" = @("RecordType","Active","LineNumber","Recorder","Period")
"AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account") "AccountingRegister" = @("PeriodAdjustment","Account","RecordType","Active","LineNumber","Recorder","Period")
"CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry") "CalculationRegister" = @("RegistrationPeriod","ReversingEntry","Active","EndOfBasePeriod","BegOfBasePeriod","EndOfActionPeriod","BegOfActionPeriod","ActionPeriod","CalculationType","LineNumber","Recorder")
"ChartOfAccounts" = @("PredefinedDataName","Order","OffBalance","Type","Description","Code","Parent","Predefined","DeletionMark","Ref") "ChartOfAccounts" = @("PredefinedDataName","Order","OffBalance","Type","Description","Code","Parent","Predefined","DeletionMark","Ref")
"ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","ValueType") "ChartOfCharacteristicTypes" = @("PredefinedDataName","ValueType","Description","Code","IsFolder","Parent","Predefined","DeletionMark","Ref")
"ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","ActionPeriodIsBasic","Description","Code") "ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","ActionPeriodIsBasic","Description","Code")
"BusinessProcess" = @("Ref","DeletionMark","Date","Number","Started","Completed","HeadTask") "BusinessProcess" = @("Started","HeadTask","Completed","Ref","DeletionMark","Date","Number")
"Task" = @("Ref","DeletionMark","Date","Number","Executed","Description","RoutePoint","BusinessProcess") "Task" = @("Executed","Description","RoutePoint","BusinessProcess","Ref","DeletionMark","Date","Number")
"ExchangePlan" = @("Ref","DeletionMark","Code","Description","ThisNode","SentNo","ReceivedNo") # Порядок в каждом списке — канон выгрузки, снят с корпуса acc+erp (внутри типа разброса нет).
# Условные члены перечислены в $script:stdAttrConditions — позицию они берут отсюда, а
# присутствие определяется свойствами объекта.
# У ПВХ IsFolder входит в фикс-список: он есть у всех 23 объектов корпуса с этим блоком.
# У регистра расчёта список безусловен: реквизиты периода действия и базового периода
# платформа пишет при любых ActionPeriod/BasePeriod/Periodicity (синтетика, все 4 комбинации).
"ExchangePlan" = @("ThisNode","ReceivedNo","SentNo","Ref","DeletionMark","Description","Code")
"DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number") "DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number")
} }
@@ -1372,6 +1449,55 @@ function Emit-StandardAttribute {
# - stdAttrProfile[тип]: профиль материализованного блока (пусто = schema-дефолт), поверх — DSL-override. # - stdAttrProfile[тип]: профиль материализованного блока (пусто = schema-дефолт), поверх — DSL-override.
# Миграция типа = добавить его в stdAttrConditionalTypes + stdAttrProfile и переснять снэпшоты; КОД НЕ ТРОГАЕМ. # Миграция типа = добавить его в stdAttrConditionalTypes + stdAttrProfile и переснять снэпшоты; КОД НЕ ТРОГАЕМ.
$script:stdAttrConditionalTypes = @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document') $script:stdAttrConditionalTypes = @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document')
# Условные члены списка типа: позиция берётся из $script:standardAttributesByType, а присутствие —
# из свойств самого объекта, как у платформы. Предикат ОБЯЗАН принимать определение параметром:
# scriptblock не видит $def вызывающей функции, и отказ был бы молчаливым.
$script:stdAttrConditions = @{
"AccountingRegister" = @{
"PeriodAdjustment" = { param($d) $v = 0; if ($null -ne $d.periodAdjustmentLength) { $v = [int]"$($d.periodAdjustmentLength)" }; $v -gt 0 }
"RecordType" = { param($d) -not ($d.correspondence -eq $true) }
}
"AccumulationRegister" = @{
"RecordType" = { param($d) $raw = if ($d.registerType) { "$($d.registerType)" } else { "Balance" }; (Normalize-EnumValue "RegisterType" $raw) -eq "Balance" }
}
}
# Хвостовая группа: реквизиты, которых нет в списке типа и которые идут ПОСЛЕ него.
# У бухрегистра это пары субконто. Именами их не перечислить: их количество задаётся
# свойством MaxExtDimensionCount плана счетов, а не константой (в корпусе везде 3, но
# это однородность выборки, а не правило). Поэтому — шаблон, а не список.
$script:stdAttrTailPattern = @{
"AccountingRegister" = '^ExtDimension(Type)?\d+$'
}
# Состав хвоста задаёт не DSL, а объект, на который регистр ссылается: пар субконто столько,
# сколько у плана счетов MaxExtDimensionCount. Читаем его из выгрузки — как версию формата из
# Configuration.xml, — чтобы регистр, описанный неполным DSL, совпал с тем, что материализует
# платформа. План не найден → хвост не генерируем и говорим об этом в выводе.
$script:stdAttrTailHint = $null
$script:stdAttrTailDerived = @{
"AccountingRegister" = {
param($d, $objectName, $outDir)
$ref = "$($d.chartOfAccounts)"
if (-not $ref) { return @() }
$chartName = $ref -replace '^.*\.', '' # ссылка вида ChartOfAccounts.X (имя объекта точек не содержит)
$path = Join-Path (Join-Path $outDir "ChartsOfAccounts") "$chartName.xml"
if (-not (Test-Path -LiteralPath $path)) {
$script:stdAttrTailHint = "ChartOfAccounts '$chartName' not found in dump — ExtDimension pairs not generated (platform will add them on load)"
return @()
}
$n = 0
if ([System.IO.File]::ReadAllText($path) -match '<MaxExtDimensionCount>(\d+)</MaxExtDimensionCount>') { $n = [int]$matches[1] }
$out = @()
for ($i = 1; $i -le $n; $i++) {
# ExtDimensionN связан с Account через LinkByType (LinkItem = номер), ExtDimensionTypeN — нет.
$out += @{ name = "ExtDimension$i"; ov = @{ LinkByType = @{ dataPath = "AccountingRegister.$objectName.StandardAttribute.Account"; linkItem = $i } } }
$out += @{ name = "ExtDimensionType$i"; ov = @{} }
}
return $out
}
}
function Emit-StandardAttributes { function Emit-StandardAttributes {
param([string]$indent, [string]$objectType) param([string]$indent, [string]$objectType)
$attrs = $script:standardAttributesByType[$objectType] $attrs = $script:standardAttributesByType[$objectType]
@@ -1381,14 +1507,45 @@ function Emit-StandardAttributes {
if ($conditional -and $null -eq $sa) { return } # условный тип без кастомизации → блока нет if ($conditional -and $null -eq $sa) { return } # условный тип без кастомизации → блока нет
if ($sa -is [string] -and $sa -eq '') { return } # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок — правило не выводимо) if ($sa -is [string] -and $sa -eq '') { return } # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок — правило не выводимо)
$profile = $script:stdAttrProfile[$objectType]; if (-not $profile) { $profile = @{} } $profile = $script:stdAttrProfile[$objectType]; if (-not $profile) { $profile = @{} }
# Доп. (опциональные) стандартные реквизиты вне фикс-списка типа — напр. ExchangeDate у части ПланОбмена # Список типа задаёт ПОРЯДОК всех известных стандартных реквизитов, включая условные:
# (легаси, присутствие не выводится из свойств). Эмитим по факту наличия ключа в DSL, ПЕРЕД фикс-списком (их позиция). # их позиция бывает и до, и после обязательных (у бухрегистра PeriodAdjustment идёт
$extra = @() # перед Account, RecordType — после, а ExtDimension1..3/ExtDimensionType1..3 — после Period),
if ($sa) { foreach ($k in $sa.PSObject.Properties.Name) { if ($attrs -notcontains $k) { $extra += $k } } } # поэтому «условные скопом вперёд» не выражает канон.
$cond = $script:stdAttrConditions[$objectType]
# Ключи, которых нет в списке типа ВООБЩЕ. По умолчанию их позиция — ПЕРЕД списком
# (легаси вроде ExchangeDate у части планов обмена). Подходящие под хвостовой шаблон
# типа идут ПОСЛЕ, в порядке номера, а внутри номера — сначала ExtDimensionN, затем
# ExtDimensionTypeN (порядок платформы).
$tailRe = $script:stdAttrTailPattern[$objectType]
$extra = @(); $tail = @()
if ($sa) {
foreach ($k in $sa.PSObject.Properties.Name) {
if ($attrs -contains $k) { continue }
if ($tailRe -and $k -match $tailRe) { $tail += $k } else { $extra += $k }
}
}
# Хвост, выведенный из связанного объекта: дополняет DSL, а не заменяет его — лишнее из DSL
# остаётся (прощаем), недостающее добавляется вместе со своими значениями по умолчанию.
$derivedOv = @{}
$gen = $script:stdAttrTailDerived[$objectType]
if ($gen) {
foreach ($e in @(& $gen $def $objName $OutputDir)) {
$derivedOv[$e.name] = $e.ov
if ($tail -notcontains $e.name) { $tail += $e.name }
}
}
$tail = @($tail | Sort-Object @{e={[int]([regex]::Match($_, '\d+').Value)}}, @{e={ if ($_ -match 'Type\d+$') { 1 } else { 0 } }})
X "$indent<StandardAttributes>" X "$indent<StandardAttributes>"
foreach ($a in ($extra + $attrs)) { foreach ($a in ($extra + $attrs + $tail)) {
# Условный реквизит: эмитим, если так велят свойства объекта ЛИБО если ключ есть в DSL.
# Дизъюнкция страхует роундтрип — декомпилятор перечисляет все имена блока.
if ($cond -and $cond.ContainsKey($a)) {
$present = ($sa -and $sa.PSObject.Properties[$a]) -or (& $cond[$a] $def)
if (-not $present) { continue }
}
$ov = @{} $ov = @{}
if ($profile.ContainsKey($a)) { foreach ($k in $profile[$a].Keys) { $ov[$k] = $profile[$a][$k] } } if ($profile.ContainsKey($a)) { foreach ($k in $profile[$a].Keys) { $ov[$k] = $profile[$a][$k] } }
if ($derivedOv.ContainsKey($a)) { foreach ($k in $derivedOv[$a].Keys) { $ov[$k] = $derivedOv[$a][$k] } }
if ($sa) { # DSL-override применяем всегда при наличии ключа (для не-условных типов тоже, напр. ExchangePlan) if ($sa) { # DSL-override применяем всегда при наличии ключа (для не-условных типов тоже, напр. ExchangePlan)
$d = $sa.$a $d = $sa.$a
if ($d) { if ($d) {
@@ -1946,9 +2103,12 @@ function Emit-Attribute {
} }
} }
# Use — only for catalog top-level attributes # Use — у реквизитов справочника и ПВХ. Позиция РАЗНАЯ: справочник пишет Use ПЕРЕД
# Indexing, ПВХ — ПОСЛЕ него (корпус acc+erp: Catalog `Use,Indexing,FullTextSearch`,
# ПВХ `Indexing,Use,FullTextSearch,DataHistory`). Отсюда отдельный контекст "cct":
# структурно реквизит ПВХ совпадает со справочником, расходится только этим порядком.
$use = if ($parsed.use) { $parsed.use } else { "ForItem" }
if ($context -eq "catalog") { if ($context -eq "catalog") {
$use = if ($parsed.use) { $parsed.use } else { "ForItem" }
X "$indent`t`t<Use>$use</Use>" X "$indent`t`t<Use>$use</Use>"
} }
@@ -1964,6 +2124,7 @@ function Emit-Attribute {
if ($parsed.indexing) { $indexing = $parsed.indexing } if ($parsed.indexing) { $indexing = $parsed.indexing }
X "$indent`t`t<Indexing>$indexing</Indexing>" X "$indent`t`t<Indexing>$indexing</Indexing>"
} }
if ($context -eq "cct") { X "$indent`t`t<Use>$use</Use>" }
# Реквизит адресации задачи: AddressingDimension (ссылка на измерение регистра исполнителей), между Indexing и FullTextSearch. # Реквизит адресации задачи: AddressingDimension (ссылка на измерение регистра исполнителей), между Indexing и FullTextSearch.
if ($context -eq "task-addressing" -and $elemTag -eq "AddressingAttribute") { if ($context -eq "task-addressing" -and $elemTag -eq "AddressingAttribute") {
@@ -2131,6 +2292,11 @@ function Emit-EnumValue {
X "$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>" X "$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>"
Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym Emit-MLText "$indent`t`t" "Synonym" $parsed.synonym
if ($parsed.comment) { X "$indent`t`t<Comment>$(Esc-XmlText $parsed.comment)</Comment>" } else { X "$indent`t`t<Comment/>" } if ($parsed.comment) { X "$indent`t`t<Comment>$(Esc-XmlText $parsed.comment)</Comment>" } else { X "$indent`t`t<Comment/>" }
# Цвет значения перечисления — свойство формата 2.21 (8.5), последним в Properties.
if ($script:isFormat221) {
$color = if ($parsed.color) { "$($parsed.color)" } else { "auto" }
X "$indent`t`t<Color>$(Esc-XmlText $color)</Color>"
}
X "$indent`t</Properties>" X "$indent`t</Properties>"
X "$indent</EnumValue>" X "$indent</EnumValue>"
} }
@@ -2797,6 +2963,11 @@ function Emit-CommonFormProperties {
} else { } else {
X "$i<UsePurposes/>" X "$i<UsePurposes/>"
} }
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
# между UsePurposes и UseStandardCommands.
if ($script:isFormat221) {
X "$i<UseInInterfaceCompatibilityMode>$(Get-EnumProp 'UseInInterfaceCompatibilityMode' 'useInInterfaceCompatibilityMode' 'Any')</UseInInterfaceCompatibilityMode>"
}
$useStdCmds = if (Get-BoolProp "useStandardCommands" $false) { "true" } else { "false" } $useStdCmds = if (Get-BoolProp "useStandardCommands" $false) { "true" } else { "false" }
X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>" X "$i<UseStandardCommands>$useStdCmds</UseStandardCommands>"
Emit-MLText $i "ExtendedPresentation" $def.extendedPresentation Emit-MLText $i "ExtendedPresentation" $def.extendedPresentation
@@ -3040,7 +3211,9 @@ function Emit-ScheduledJobProperties {
if ($description) { X "$i<Description>$(Esc-XmlText $description)</Description>" } else { X "$i<Description/>" } if ($description) { X "$i<Description>$(Esc-XmlText $description)</Description>" } else { X "$i<Description/>" }
$key = if ($def.key) { "$($def.key)" } else { "" } $key = if ($def.key) { "$($def.key)" } else { "" }
X "$i<Key>$(Esc-XmlText $key)</Key>" # Пустое значение → самозакрывающийся, как у <Description> выше: Конфигуратор
# не пишет пустых пар.
if ($key) { X "$i<Key>$(Esc-XmlText $key)</Key>" } else { X "$i<Key/>" }
$use = if ($def.use -eq $true) { "true" } else { "false" } $use = if ($def.use -eq $true) { "true" } else { "false" }
X "$i<Use>$use</Use>" X "$i<Use>$use</Use>"
@@ -3105,6 +3278,8 @@ function Emit-ReportProperties {
Emit-VerbatimRef $i "DefaultSettingsForm" $def.defaultSettingsForm Emit-VerbatimRef $i "DefaultSettingsForm" $def.defaultSettingsForm
Emit-VerbatimRef $i "AuxiliarySettingsForm" $def.auxiliarySettingsForm Emit-VerbatimRef $i "AuxiliarySettingsForm" $def.auxiliarySettingsForm
Emit-VerbatimRef $i "DefaultVariantForm" $def.defaultVariantForm Emit-VerbatimRef $i "DefaultVariantForm" $def.defaultVariantForm
# Вспомогательная форма варианта отчёта — свойство формата 2.21 (8.5).
if ($script:isFormat221) { Emit-VerbatimRef $i "AuxiliaryVariantForm" $def.auxiliaryVariantForm }
Emit-VerbatimRef $i "VariantsStorage" $def.variantsStorage Emit-VerbatimRef $i "VariantsStorage" $def.variantsStorage
Emit-VerbatimRef $i "SettingsStorage" $def.settingsStorage Emit-VerbatimRef $i "SettingsStorage" $def.settingsStorage
$inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" } $inclHelp = if (Get-BoolProp "includeHelpInContents" $false) { "true" } else { "false" }
@@ -3840,7 +4015,8 @@ function Emit-WebServiceProperties {
if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" } if ($def.comment) { X "$i<Comment>$(Esc-XmlText "$($def.comment)")</Comment>" } else { X "$i<Comment/>" }
$namespace = if ($def.namespace) { "$($def.namespace)" } else { "" } $namespace = if ($def.namespace) { "$($def.namespace)" } else { "" }
X "$i<Namespace>$(Esc-XmlText $namespace)</Namespace>" # Пустое значение → самозакрывающийся, как у <Comment> выше.
if ($namespace) { X "$i<Namespace>$(Esc-XmlText $namespace)</Namespace>" } else { X "$i<Namespace/>" }
# XDTOPackages — СПИСОК элементов, а не скаляр: значение либо ссылка на пакет конфигурации # XDTOPackages — СПИСОК элементов, а не скаляр: значение либо ссылка на пакет конфигурации
# (xr:MDObjectRef "XDTOPackage.Имя"), либо URI внешнего пространства имён (xs:string). # (xr:MDObjectRef "XDTOPackage.Имя"), либо URI внешнего пространства имён (xs:string).
@@ -4082,6 +4258,14 @@ $script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.
function Detect-FormatVersion([string]$dir) { function Detect-FormatVersion([string]$dir) {
$d = $dir $d = $dir
while ($d) { while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml" $cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) { if (Test-Path $cfgPath) {
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8) $cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
@@ -4143,6 +4327,15 @@ $script:compatMode = Detect-CompatibilityMode $OutputDir
# глушил бы TypeReductionMode, который платформа этих версий пишет, и роундтрип бы разъезжался. # глушил бы TypeReductionMode, который платформа этих версий пишет, и роундтрип бы разъезжался.
$script:isFormat218 = (Get-FormatRank $script:formatVersion) -ge 218 $script:isFormat218 = (Get-FormatRank $script:formatVersion) -ge 218
$script:isFormat220 = (Get-FormatRank $script:formatVersion) -ge 220 $script:isFormat220 = (Get-FormatRank $script:formatVersion) -ge 220
$script:isFormat221 = (Get-FormatRank $script:formatVersion) -ge 221
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту.
# Только для шапок MetaDataObject и Form — в файлах с корнем extrnprops
# (Ext/ClientApplicationInterface.xml и т.п.) платформа его не пишет.
if ($script:isFormat221) {
$palNs = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', "$palNs xmlns:style="
}
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами. # Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
$script:lineNumberLengthDefault = if ((Get-CompatModeRank $script:compatMode) -ge 80327) { 9 } else { 5 } $script:lineNumberLengthDefault = if ((Get-CompatModeRank $script:compatMode) -ge 80327) { 9 } else { 5 }
@@ -4268,7 +4461,7 @@ if ($objType -in $typesWithAttrTS) {
"Catalog" { "catalog" } "Catalog" { "catalog" }
"Document" { "document" } "Document" { "document" }
{ $_ -in @("DataProcessor","Report") } { "processor" } { $_ -in @("DataProcessor","Report") } { "processor" }
"ChartOfCharacteristicTypes" { "catalog" } # реквизиты ПВХ структурно как у справочника (Use/FillFromFillingValue/DataHistory) "ChartOfCharacteristicTypes" { "cct" } # как catalog (Use/FillFromFillingValue/DataHistory), но Use ПОСЛЕ Indexing
{ $_ -in @("ChartOfAccounts","ChartOfCalculationTypes") } { "account" } # как catalog, но БЕЗ <Use> (реквизиты ПС/ПВР не иерархичны как справочник) { $_ -in @("ChartOfAccounts","ChartOfCalculationTypes") } { "account" } # как catalog, но БЕЗ <Use> (реквизиты ПС/ПВР не иерархичны как справочник)
default { "object" } default { "object" }
} }
@@ -4356,16 +4549,31 @@ if ($objType -in @("InformationRegister","AccumulationRegister","AccountingRegis
$regCtx = switch ($objType) { "InformationRegister" { "register-info" } "CalculationRegister" { "register-calc" } default { "register-other" } } $regCtx = switch ($objType) { "InformationRegister" { "register-info" } "CalculationRegister" { "register-calc" } default { "register-other" } }
# Все семейства регистров: ресурсы/измерения — через богатый Emit-Attribute (общий слой object-свойств). # Все семейства регистров: ресурсы/измерения — через богатый Emit-Attribute (общий слой object-свойств).
$dimResCtx = switch ($objType) { "InformationRegister" { "register-info" } "AccumulationRegister" { "register-accum" } "CalculationRegister" { "register-calc" } "AccountingRegister" { "register-account" } default { $null } } $dimResCtx = switch ($objType) { "InformationRegister" { "register-info" } "AccumulationRegister" { "register-accum" } "CalculationRegister" { "register-calc" } "AccountingRegister" { "register-account" } default { $null } }
foreach ($r in $resources) { # Порядок видов детей — канон выгрузки, снят с корпуса (acc+erp, разброса внутри
if ($dimResCtx) { Emit-Attribute "`t`t`t" $r $dimResCtx "Resource" } # типа нет): у большинства регистров Resource, Attribute, Dimension, а у
else { Emit-Resource "`t`t`t" $r $objType } # бухгалтерского — Dimension, Resource, Attribute. Команды у платформы идут
} # последними, как и здесь.
foreach ($d in $dims) { $kindOrder = if ($objType -eq "AccountingRegister") { @('dim','res','attr') } else { @('res','attr','dim') }
if ($dimResCtx) { Emit-Attribute "`t`t`t" $d $dimResCtx "Dimension" } foreach ($kind in $kindOrder) {
else { Emit-Dimension "`t`t`t" $d $objType } switch ($kind) {
} 'res' {
foreach ($a in $regAttrs) { foreach ($r in $resources) {
Emit-Attribute "`t`t`t" $a $regCtx if ($dimResCtx) { Emit-Attribute "`t`t`t" $r $dimResCtx "Resource" }
else { Emit-Resource "`t`t`t" $r $objType }
}
}
'dim' {
foreach ($d in $dims) {
if ($dimResCtx) { Emit-Attribute "`t`t`t" $d $dimResCtx "Dimension" }
else { Emit-Dimension "`t`t`t" $d $objType }
}
}
'attr' {
foreach ($a in $regAttrs) {
Emit-Attribute "`t`t`t" $a $regCtx
}
}
}
} }
foreach ($cmd in $regCommands) { foreach ($cmd in $regCommands) {
Emit-Command "`t`t`t" $cmd.name $cmd.def Emit-Command "`t`t`t" $cmd.name $cmd.def
@@ -4609,9 +4817,13 @@ function Build-PredefinedXml {
$sb = New-Object System.Text.StringBuilder $sb = New-Object System.Text.StringBuilder
[void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n") [void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n")
[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"$xsiType`" version=`"$($script:formatVersion)`">`n") [void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"$xsiType`" version=`"$($script:formatVersion)`">`n")
foreach ($it in $items) { Emit-PredefItem $sb $it "`t" $codeType } # Шапка Predefined.xml не объявляет cfg (predef/v8/xr/xs/xsi) — на время сборки этого
# файла ссылочный тип уходит на локальную форму, как делает и платформа.
$savedCfgPrefix = $script:cfgPrefix; $script:cfgPrefix = $null
try { foreach ($it in $items) { Emit-PredefItem $sb $it "`t" $codeType } }
finally { $script:cfgPrefix = $savedCfgPrefix }
[void]$sb.Append("</PredefinedData>`n") [void]$sb.Append("</PredefinedData>`n")
return $sb.ToString() return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n"
} }
# --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/ # --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/
@@ -4697,9 +4909,12 @@ function Build-PredefinedAccountXml {
$sb = New-Object System.Text.StringBuilder $sb = New-Object System.Text.StringBuilder
[void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n") [void]$sb.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n")
[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"ChartOfAccountsPredefinedItems`" version=`"$($script:formatVersion)`">`n") [void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"ChartOfAccountsPredefinedItems`" version=`"$($script:formatVersion)`">`n")
foreach ($it in $items) { Emit-PredefAccount $sb $it "`t" $objName $acctFlagNames $extDimFlagNames $extDimTypesRef } # См. Build-PredefinedXml: шапка этого файла cfg не объявляет.
$savedCfgPrefix = $script:cfgPrefix; $script:cfgPrefix = $null
try { foreach ($it in $items) { Emit-PredefAccount $sb $it "`t" $objName $acctFlagNames $extDimFlagNames $extDimTypesRef } }
finally { $script:cfgPrefix = $savedCfgPrefix }
[void]$sb.Append("</PredefinedData>`n") [void]$sb.Append("</PredefinedData>`n")
return $sb.ToString() return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n"
} }
# --- Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase). Строка "(Код) Имя [Наим]" # --- Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase). Строка "(Код) Имя [Наим]"
@@ -4726,7 +4941,7 @@ function Build-PredefinedCalcTypeXml {
[void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"CalculationTypePredefinedItems`" version=`"$($script:formatVersion)`">`n") [void]$sb.Append("<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"CalculationTypePredefinedItems`" version=`"$($script:formatVersion)`">`n")
foreach ($it in $items) { Emit-PredefCalcType $sb $it "`t" } foreach ($it in $items) { Emit-PredefCalcType $sb $it "`t" }
[void]$sb.Append("</PredefinedData>`n") [void]$sb.Append("</PredefinedData>`n")
return $sb.ToString() return ($sb.ToString() -replace "`r`n", "`n") -replace "`n", "`r`n"
} }
$extDir = Join-Path $objSubDir "Ext" $extDir = Join-Path $objSubDir "Ext"
@@ -4741,7 +4956,20 @@ if ($objType -notin $typesNoSubDir) {
} }
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($mainXmlPath, $metadataXml, $enc)
# Единая точка записи XML. Конфигуратор не пишет перевод строки в конце файла —
# последний байт `>`; сборка через AppendLine добавляла лишний.
# KeepEol в имени — отличие от одноимённой функции в скелетных навыках
# (cf-init и др.): та ЕЩЁ и нормализует EOL к CRLF, а здесь этого делать
# НЕЛЬЗЯ — в объектном XML бывают многострочные текстовые узлы (запрос,
# синоним, значение заполнения), и сплошная нормализация меняла бы содержимое.
# Разделители тут и так CRLF: документ собран через AppendLine.
# Модули .bsl сюда НЕ идут: у них свой хвост.
function Write-XmlFileKeepEol([string]$path, [string]$text, $encoding) {
[System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFileKeepEol $mainXmlPath $metadataXml $enc
# Module files # Module files
$modulesCreated = @() $modulesCreated = @()
@@ -4822,8 +5050,10 @@ if ($objType -eq "CommonForm") {
$cfFormXmlPath = Join-Path $extDir "Form.xml" $cfFormXmlPath = Join-Path $extDir "Form.xml"
if (-not (Test-Path $cfFormXmlPath)) { if (-not (Test-Path $cfFormXmlPath)) {
$cfFormNs = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' $cfFormNs = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
$cfFormXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`n<Form $cfFormNs version=`"$($script:formatVersion)`">`n`t<AutoCommandBar name=`"ФормаКоманднаяПанель`" id=`"-1`">`n`t`t<Autofill>true</Autofill>`n`t</AutoCommandBar>`n`t<ChildItems/>`n</Form>`n" # Шапка Form на 2.21 тоже несёт палитру — см. комментарий у $script:xmlnsDecl.
[System.IO.File]::WriteAllText($cfFormXmlPath, $cfFormXml, $enc) if ($script:isFormat221) { $cfFormNs = $cfFormNs -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=' }
$cfFormXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Form $cfFormNs version=`"$($script:formatVersion)`">`r`n`t<AutoCommandBar name=`"ФормаКоманднаяПанель`" id=`"-1`">`r`n`t`t<Autofill>true</Autofill>`r`n`t</AutoCommandBar>`r`n`t<ChildItems/>`r`n</Form>`r`n"
Write-XmlFileKeepEol $cfFormXmlPath $cfFormXml $enc
$modulesCreated += $cfFormXmlPath $modulesCreated += $cfFormXmlPath
} }
$cfModuleDir = Join-Path $extDir "Form" $cfModuleDir = Join-Path $extDir "Form"
@@ -4876,7 +5106,7 @@ if ($objType -eq "ExchangePlan") {
[void]$sbC.Append("`t</Item>`r`n") [void]$sbC.Append("`t</Item>`r`n")
} }
[void]$sbC.Append("</ExchangePlanContent>`r`n") [void]$sbC.Append("</ExchangePlanContent>`r`n")
[System.IO.File]::WriteAllText($contentPath, $sbC.ToString(), $enc) Write-XmlFileKeepEol $contentPath $sbC.ToString() $enc
$modulesCreated += $contentPath $modulesCreated += $contentPath
} elseif (-not (Test-Path $contentPath)) { } elseif (-not (Test-Path $contentPath)) {
# При пустом составе платформа всё равно пишет пустой <ExchangePlanContent/> — проверено на # При пустом составе платформа всё равно пишет пустой <ExchangePlanContent/> — проверено на
@@ -4884,7 +5114,7 @@ if ($objType -eq "ExchangePlan") {
# (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.) # (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.)
Ensure-ExtDir Ensure-ExtDir
$contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent $xepNs version=`"$($script:formatVersion)`"/>`r`n" $contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent $xepNs version=`"$($script:formatVersion)`"/>`r`n"
[System.IO.File]::WriteAllText($contentPath, $contentXml, $enc) Write-XmlFileKeepEol $contentPath $contentXml $enc
$modulesCreated += $contentPath $modulesCreated += $contentPath
} }
} }
@@ -4893,7 +5123,7 @@ if ($objType -eq "BusinessProcess") {
if (-not (Test-Path $flowchartPath)) { if (-not (Test-Path $flowchartPath)) {
Ensure-ExtDir Ensure-ExtDir
$flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`" version=`"$($script:formatVersion)`"/>`r`n" $flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`" version=`"$($script:formatVersion)`"/>`r`n"
[System.IO.File]::WriteAllText($flowchartPath, $flowchartXml, $enc) Write-XmlFileKeepEol $flowchartPath $flowchartXml $enc
$modulesCreated += $flowchartPath $modulesCreated += $flowchartPath
} }
} }
@@ -4908,20 +5138,20 @@ if ($objType -eq 'ChartOfAccounts' -and $def.predefined -and @($def.predefined).
$edtRef = if ($def.extDimensionTypes) { Resolve-TypePrefixSyn "$($def.extDimensionTypes)" } else { '' } $edtRef = if ($def.extDimensionTypes) { Resolve-TypePrefixSyn "$($def.extDimensionTypes)" } else { '' }
$predefXml = Build-PredefinedAccountXml @($def.predefined) $objName $afNames $edfNames $edtRef $predefXml = Build-PredefinedAccountXml @($def.predefined) $objName $afNames $edfNames $edtRef
$predefPath = Join-Path $extDir "Predefined.xml" $predefPath = Join-Path $extDir "Predefined.xml"
[System.IO.File]::WriteAllText($predefPath, $predefXml, $enc) Write-XmlFileKeepEol $predefPath $predefXml $enc
$modulesCreated += $predefPath $modulesCreated += $predefPath
} elseif ($objType -eq 'ChartOfCalculationTypes' -and $def.predefined -and @($def.predefined).Count -gt 0) { } elseif ($objType -eq 'ChartOfCalculationTypes' -and $def.predefined -and @($def.predefined).Count -gt 0) {
Ensure-ExtDir Ensure-ExtDir
$predefXml = Build-PredefinedCalcTypeXml @($def.predefined) $predefXml = Build-PredefinedCalcTypeXml @($def.predefined)
$predefPath = Join-Path $extDir "Predefined.xml" $predefPath = Join-Path $extDir "Predefined.xml"
[System.IO.File]::WriteAllText($predefPath, $predefXml, $enc) Write-XmlFileKeepEol $predefPath $predefXml $enc
$modulesCreated += $predefPath $modulesCreated += $predefPath
} elseif ($predefRootByType.ContainsKey($objType) -and $def.predefined -and @($def.predefined).Count -gt 0) { } elseif ($predefRootByType.ContainsKey($objType) -and $def.predefined -and @($def.predefined).Count -gt 0) {
Ensure-ExtDir Ensure-ExtDir
$catCodeType = if ($def.codeType) { "$($def.codeType)" } else { 'String' } $catCodeType = if ($def.codeType) { "$($def.codeType)" } else { 'String' }
$predefXml = Build-PredefinedXml @($def.predefined) $predefRootByType[$objType] $catCodeType $predefXml = Build-PredefinedXml @($def.predefined) $predefRootByType[$objType] $catCodeType
$predefPath = Join-Path $extDir "Predefined.xml" $predefPath = Join-Path $extDir "Predefined.xml"
[System.IO.File]::WriteAllText($predefPath, $predefXml, $enc) Write-XmlFileKeepEol $predefPath $predefXml $enc
$modulesCreated += $predefPath $modulesCreated += $predefPath
} }
@@ -4990,15 +5220,31 @@ if (Test-Path $configXmlPath) {
} }
} }
# Save # Save. Пишем через MemoryStream, а не прямо в файл: нужен шаг пост-обработки
# строки — XmlWriter отдаёт `encoding="utf-8"` и `<a />`, Конфигуратор пишет
# `encoding="UTF-8"` и `<a/>`.
$cfgSettings = New-Object System.Xml.XmlWriterSettings $cfgSettings = New-Object System.Xml.XmlWriterSettings
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true) $cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
$cfgSettings.Indent = $false $cfgSettings.Indent = $false
$stream = New-Object System.IO.FileStream($configXmlPath, [System.IO.FileMode]::Create) $cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
$writer = [System.Xml.XmlWriter]::Create($stream, $cfgSettings) $memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
$configDoc.Save($writer) $configDoc.Save($writer)
$writer.Close() $writer.Flush(); $writer.Close()
$stream.Close()
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$cfgText = [regex]::Replace($cfgText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $configXmlPath) -and ([System.IO.File]::ReadAllText($configXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($configXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
$regResult = "added" $regResult = "added"
} }
@@ -5059,6 +5305,9 @@ switch ($regResult) {
} }
# Cross-reference hints # Cross-reference hints
if ($script:stdAttrTailHint) {
Write-Host "[HINT] $($script:stdAttrTailHint)"
}
if ($objType -eq "AccountingRegister" -and -not $def.chartOfAccounts) { if ($objType -eq "AccountingRegister" -and -not $def.chartOfAccounts) {
Write-Host "[HINT] AccountingRegister requires ChartOfAccounts reference:" Write-Host "[HINT] AccountingRegister requires ChartOfAccounts reference:"
Write-Host " /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`"" Write-Host " /meta-edit -Operation modify-property -Value `"ChartOfAccounts=ChartOfAccounts.XXX`""
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# meta-compile v1.76 — Compile 1C metadata object from JSON # meta-compile v1.94 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -16,6 +16,69 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
# ============================================================
# Регистронезависимый ввод — паритет с PS1. В PowerShell регистр не значим нигде, куда
# смотрит пользовательский ввод: свойства объекта из ConvertFrom-Json, ключи Hashtable,
# -eq/-contains, имена параметров, ValidateSet. В Python совпадение точное, поэтому порт
# молча терял свойства DSL, написанные в другом регистре. Обёртки ниже выравнивают поведение.
# ============================================================
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================ # ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin # Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
@@ -206,9 +269,22 @@ def new_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
def write_utf8_bom(path, content): def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
def write_xml_file_keep_eol(path, content):
# Единая точка записи XML. Конфигуратор не пишет перевод строки в конце файла —
# последний байт `>`.
# keep_eol в имени — отличие от одноимённой функции в скелетных навыках
# (cf-init и др.): та ЕЩЁ и нормализует EOL к CRLF, а здесь этого делать
# НЕЛЬЗЯ — в объектном XML бывают многострочные текстовые узлы (запрос,
# синоним, значение заполнения). Разделители и так CRLF — их даёт join строк.
# Модули .bsl сюда НЕ идут: у них свой хвост.
write_utf8_bom(path, content.rstrip('\r\n'))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# XML builder (lines list) # XML builder (lines list)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -289,7 +365,7 @@ def split_camel_case(name):
parser = argparse.ArgumentParser(allow_abbrev=False) parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument('-JsonPath', required=True) parser.add_argument('-JsonPath', required=True)
parser.add_argument('-OutputDir', required=True) parser.add_argument('-OutputDir', required=True)
args = parser.parse_args() args = ci_parse_args(parser)
json_path = args.JsonPath json_path = args.JsonPath
output_dir = args.OutputDir output_dir = args.OutputDir
@@ -301,7 +377,7 @@ if not os.path.isfile(json_path):
with open(json_path, 'r', encoding='utf-8-sig') as f: with open(json_path, 'r', encoding='utf-8-sig') as f:
json_text = f.read() json_text = f.read()
defn = json.loads(json_text) defn = ci_json(json.loads(json_text))
assert_edit_allowed(output_dir, "editable") assert_edit_allowed(output_dir, "editable")
@@ -395,6 +471,10 @@ enum_value_aliases = {
'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder', 'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder',
} }
# Словари, по которым ищут ПОЛЬЗОВАТЕЛЬСКИЙ ввод, — регистронезависимы, как хеш-таблицы PS1.
object_type_synonyms = CIDict(object_type_synonyms)
enum_value_aliases = CIDict(enum_value_aliases)
# Valid enum values per property (from meta-validate) # Valid enum values per property (from meta-validate)
valid_enum_values = { valid_enum_values = {
'RegisterType': ['Balance', 'Turnovers'], 'RegisterType': ['Balance', 'Turnovers'],
@@ -553,6 +633,8 @@ valid_types = [
'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference',
'CommonPicture', 'CommonTemplate', 'CommonPicture', 'CommonTemplate',
] ]
# Регистр имени вида — как в PS (-contains регистронезависим): приводим к канону списка
obj_type = next((t for t in valid_types if t.lower() == obj_type.lower()), obj_type)
if obj_type not in valid_types: if obj_type not in valid_types:
print(f"Unsupported type: {obj_type}. Valid: {', '.join(valid_types)}", file=sys.stderr) print(f"Unsupported type: {obj_type}. Valid: {', '.join(valid_types)}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -627,47 +709,103 @@ type_namespace_map = {
"SpreadsheetDocument": {"ns": "http://v8.1c.ru/8.2/data/spreadsheet", "prefix": "mxl"}, "SpreadsheetDocument": {"ns": "http://v8.1c.ru/8.2/data/spreadsheet", "prefix": "mxl"},
} }
# Типы current-config пространства (cfg:, объявлено в корне): голые и объектные. Ссылочные — отдельно (d5p1). # Типы current-config пространства (cfg:, объявлено в корне): голые и объектные. Ссылочные — отдельно (d5p1).
# Префикс current-config для ссылочных типов. 'cfg' — для файлов, чья шапка его объявляет
# (объектный XML, Ext/Form.xml общей формы). None на время сборки Ext/Predefined.xml, чья
# шапка его НЕ объявляет: там и платформа уходит на локальное объявление.
cfg_prefix = 'cfg'
cfg_bare_types = {"ConstantsSet", "ReportBuilder", "FilterCriterion"} cfg_bare_types = {"ConstantsSet", "ReportBuilder", "FilterCriterion"}
cfg_object_kinds = {"Catalog", "Document", "Enum", "ChartOfAccounts", "ChartOfCharacteristicTypes", cfg_object_kinds = {"Catalog", "Document", "Enum", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ChartOfCalculationTypes", "ExchangePlan", "BusinessProcess", "Task", "InformationRegister", "ChartOfCalculationTypes", "ExchangePlan", "BusinessProcess", "Task", "InformationRegister",
"AccumulationRegister", "AccountingRegister", "CalculationRegister", "DataProcessor", "Report", "AccumulationRegister", "AccountingRegister", "CalculationRegister", "DataProcessor", "Report",
"DocumentJournal", "Constant", "ConstantValue", "Sequence", "Recalculation"} "DocumentJournal", "Constant", "ConstantValue", "Sequence", "Recalculation"}
# Алиас на локальный словарь: тело resolve_type_str ниже — общая реализация,
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
type_synonyms = CIDict(type_synonyms)
TYPE_SYNONYMS = type_synonyms
def resolve_type_str(type_str): def resolve_type_str(type_str):
if not type_str: if not type_str:
return type_str return type_str
# Parameterized types: Number(15,2), Строка(100), etc. # Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
if type_str.startswith('cfg:'):
type_str = type_str[4:]
elif '.' in type_str and re.match(r'^d\d+p\d+:', type_str):
type_str = type_str[type_str.index(':') + 1:]
# Параметризованные типы: Number(15,2), Строка(100)
m = re.match(r'^([^(]+)\((.+)\)$', type_str) m = re.match(r'^([^(]+)\((.+)\)$', type_str)
if m: if m:
base_name = m.group(1).strip() base_name = m.group(1).strip()
params = m.group(2) params = m.group(2)
resolved = type_synonyms.get(base_name.lower()) resolved = TYPE_SYNONYMS.get(base_name.lower())
if resolved: if resolved:
return f'{resolved}({params})' return f'{resolved}({params})'
return type_str return type_str
# Reference types: СправочникСсылка.Организации -> CatalogRef.Организации # Ссылочные типы: СправочникСсылка.Организации -> CatalogRef.Организации
if '.' in type_str: if '.' in type_str:
dot_idx = type_str.index('.') dot_idx = type_str.index('.')
prefix = type_str[:dot_idx] prefix = type_str[:dot_idx]
suffix = type_str[dot_idx:] # includes the dot suffix = type_str[dot_idx:] # includes the dot
resolved = type_synonyms.get(prefix.lower()) resolved = TYPE_SYNONYMS.get(prefix.lower())
if resolved: if resolved:
return f'{resolved}{suffix}' return f'{resolved}{suffix}'
return type_str return type_str
# Simple name lookup # Простое имя
resolved = type_synonyms.get(type_str.lower()) resolved = TYPE_SYNONYMS.get(type_str.lower())
if resolved: if resolved:
return resolved return resolved
return type_str return type_str
def emit_type_content(indent, type_str): def emit_type_content(indent, type_str):
if not type_str: if not type_str:
return return
# Composite type: "Type1 + Type2 + Type3" # Composite type: "Type1 + Type2 + Type3"
# Платформа пишет сначала ВСЕ <v8:Type>/<v8:TypeSet>, и только потом блоки
# квалификаторов — а рекурсия ниже печатала бы каждую часть целиком (тип вместе со
# своими квалификаторами). На одиночном типе оба порядка совпадают, поэтому
# расхождение вылезало только на составном.
# Порядок самих блоков квалификаторов тоже канонический и НЕ зеркалит порядок типов:
# Number, String, Date (корпус acc+erp, контрпримеров нет — при типах
# boolean,string,dateTime,decimal квалификаторы идут Number,String,Date).
# Порядок типов при этом сохраняем как в DSL: он и есть порядок источника.
if ' + ' in type_str: if ' + ' in type_str:
parts = [p.strip() for p in type_str.split('+')] parts = [p.strip() for p in type_str.split('+')]
type_lines = []
qual_blocks = {}
for part in parts: for part in parts:
# X добавляет в список lines, поэтому «перехват» — это срез и откат хвоста.
# В PS-порту X пишет в StringBuilder и тот же алгоритм выражен через
# Length/Remove — различие рантаймов, не логики.
before = len(lines)
emit_type_content(indent, part) emit_type_content(indent, part)
chunk = lines[before:]
del lines[before:]
cur_qual = None
for line in chunk:
if not line:
continue
m = re.search(r'<v8:(String|Number|Date)Qualifiers>', line)
if m:
cur_qual = m.group(1)
qual_blocks[cur_qual] = []
if cur_qual:
qual_blocks[cur_qual].append(line)
if re.search(r'</v8:(String|Number|Date)Qualifiers>', line):
cur_qual = None
else:
type_lines.append(line)
for line in type_lines:
X(line)
for q in ('Number', 'String', 'Date'):
if q in qual_blocks:
for line in qual_blocks[q]:
X(line)
return return
type_str = resolve_type_str(type_str) type_str = resolve_type_str(type_str)
# Boolean # Boolean
@@ -760,10 +898,22 @@ def emit_type_content(indent, type_str):
X(f'{indent}<v8:Type>cfg:{type_str}</v8:Type>') X(f'{indent}<v8:Type>cfg:{type_str}</v8:Type>')
return return
# Reference types — use local xmlns declaration for 1C compatibility # Ссылочные типы — корневой cfg:, как пишет платформа. Раньше здесь объявлялся
# ЛОКАЛЬНЫЙ xmlns:d5p1 на тот же URI, что уже объявлен в шапке: формально
# эквивалентно (значим URI, не префикс) и платформой принималось, но первый же
# цикл «загрузить в базу → выгрузить» переписывал каждый ссылочный тип в cfg: —
# то есть давал diff-шум на ровном месте. Форма пришла из СКД, где cfg:
# действительно не работает; в метаданных такого ограничения нет.
# NB: локальная xmlns остаётся законной для ЧУЖИХ пространств — см. type_namespace_map.
# cfg_prefix = None означает «пишем файл, корень которого cfg НЕ объявляет»
# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$', type_str) m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$', type_str)
if m: if m:
X(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{type_str}</v8:Type>') if cfg_prefix:
X(f'{indent}<v8:Type>{cfg_prefix}:{type_str}</v8:Type>')
else:
X(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{type_str}</v8:Type>')
return return
# Fallback # Fallback
X(f'{indent}<v8:Type>{type_str}</v8:Type>') X(f'{indent}<v8:Type>{type_str}</v8:Type>')
@@ -1270,15 +1420,24 @@ standard_attributes_by_type = {
'Document': ['Posted', 'Ref', 'DeletionMark', 'Date', 'Number'], 'Document': ['Posted', 'Ref', 'DeletionMark', 'Date', 'Number'],
'Enum': ['Order', 'Ref'], 'Enum': ['Order', 'Ref'],
'InformationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'], 'InformationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'],
'AccumulationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'], 'AccumulationRegister': ['RecordType', 'Active', 'LineNumber', 'Recorder', 'Period'],
'AccountingRegister': ['Active', 'Period', 'Recorder', 'LineNumber', 'Account'], 'AccountingRegister': ['PeriodAdjustment', 'Account', 'RecordType', 'Active', 'LineNumber', 'Recorder', 'Period'],
'CalculationRegister': ['Active', 'Recorder', 'LineNumber', 'RegistrationPeriod', 'CalculationType', 'ReversingEntry'], 'CalculationRegister': ['RegistrationPeriod', 'ReversingEntry', 'Active', 'EndOfBasePeriod', 'BegOfBasePeriod', 'EndOfActionPeriod', 'BegOfActionPeriod', 'ActionPeriod', 'CalculationType', 'LineNumber', 'Recorder'],
'ChartOfAccounts': ['PredefinedDataName', 'Order', 'OffBalance', 'Type', 'Description', 'Code', 'Parent', 'Predefined', 'DeletionMark', 'Ref'], 'ChartOfAccounts': ['PredefinedDataName', 'Order', 'OffBalance', 'Type', 'Description', 'Code', 'Parent', 'Predefined', 'DeletionMark', 'Ref'],
'ChartOfCharacteristicTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'Description', 'Code', 'Parent', 'ValueType'], 'ChartOfCharacteristicTypes': ['PredefinedDataName', 'ValueType', 'Description', 'Code', 'IsFolder', 'Parent', 'Predefined', 'DeletionMark', 'Ref'],
'ChartOfCalculationTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'ActionPeriodIsBasic', 'Description', 'Code'], 'ChartOfCalculationTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'ActionPeriodIsBasic', 'Description', 'Code'],
'BusinessProcess': ['Ref', 'DeletionMark', 'Date', 'Number', 'Started', 'Completed', 'HeadTask'], # Порядок в каждом списке — канон выгрузки, снят с корпуса acc+erp (внутри типа разброса нет).
'Task': ['Ref', 'DeletionMark', 'Date', 'Number', 'Executed', 'Description', 'RoutePoint', 'BusinessProcess'], # Условные члены перечислены в std_attr_conditions — позицию они берут отсюда, а
'ExchangePlan': ['Ref', 'DeletionMark', 'Code', 'Description', 'ThisNode', 'SentNo', 'ReceivedNo'], # присутствие определяется свойствами объекта.
# У ПВХ IsFolder входит в фикс-список: он есть у всех 23 объектов корпуса с этим блоком.
# У регистра расчёта список безусловен: реквизиты периода действия и базового периода
# платформа пишет при любых ActionPeriod/BasePeriod/Periodicity (синтетика, все 4 комбинации).
'BusinessProcess': ['Started', 'HeadTask', 'Completed', 'Ref', 'DeletionMark', 'Date', 'Number'],
'Task': ['Executed', 'Description', 'RoutePoint', 'BusinessProcess', 'Ref', 'DeletionMark', 'Date', 'Number'],
# Порядок снят с выгрузки: у плана обмена блок начинается с ThisNode, а не с Ref
# (acc+erp, 8 объектов, разброса нет). Прочие типы в этой таблице совпадают с
# платформой — расхождений порядка по ним корпусный раундтрип не показал.
'ExchangePlan': ['ThisNode', 'ReceivedNo', 'SentNo', 'Ref', 'DeletionMark', 'Description', 'Code'],
'DocumentJournal': ['Type', 'Ref', 'Date', 'Posted', 'DeletionMark', 'Number'], 'DocumentJournal': ['Type', 'Ref', 'Date', 'Posted', 'DeletionMark', 'Number'],
} }
@@ -1398,6 +1557,64 @@ def emit_standard_attribute(indent, attr_name, ov=None):
# std_attr_conditional_types: типы, где блок только при кастомизации (DSL-ключ standardAttributes). # std_attr_conditional_types: типы, где блок только при кастомизации (DSL-ключ standardAttributes).
# Прочие типы → блок всегда (текущее поведение). Миграция типа = +строчка в оба справочника + снэпшоты. # Прочие типы → блок всегда (текущее поведение). Миграция типа = +строчка в оба справочника + снэпшоты.
std_attr_conditional_types = {'Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document'} std_attr_conditional_types = {'Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document'}
# Условные члены списка типа: позиция берётся из standard_attributes_by_type, а присутствие —
# из свойств самого объекта, как у платформы. Предикат принимает определение параметром
# (зеркало .ps1, где scriptblock не видит $def вызывающей функции).
def _period_adjustment_used(d):
v = d.get('periodAdjustmentLength')
return v is not None and int(str(v)) > 0
std_attr_conditions = {
'AccountingRegister': {
'PeriodAdjustment': _period_adjustment_used,
'RecordType': lambda d: d.get('correspondence') is not True,
},
'AccumulationRegister': {
'RecordType': lambda d: normalize_enum_value('RegisterType', str(d.get('registerType') or 'Balance')) == 'Balance',
},
}
# Хвостовая группа: реквизиты, которых нет в списке типа и которые идут ПОСЛЕ него.
# У бухрегистра это пары субконто. Именами их не перечислить: их количество задаётся
# свойством MaxExtDimensionCount плана счетов, а не константой (в корпусе везде 3, но
# это однородность выборки, а не правило). Поэтому — шаблон, а не список.
std_attr_tail_pattern = {
'AccountingRegister': r'^ExtDimension(Type)?\d+$',
}
# Состав хвоста задаёт не DSL, а объект, на который регистр ссылается: пар субконто столько,
# сколько у плана счетов MaxExtDimensionCount. Читаем его из выгрузки — как версию формата из
# Configuration.xml, — чтобы регистр, описанный неполным DSL, совпал с тем, что материализует
# платформа. План не найден → хвост не генерируем и говорим об этом в выводе.
std_attr_tail_hint = None
def _acc_register_ext_dimension_tail(d, object_name, out_dir):
global std_attr_tail_hint
ref = str(d.get('chartOfAccounts') or '')
if not ref:
return []
chart_name = re.sub(r'^.*\.', '', ref) # ссылка вида ChartOfAccounts.X (имя объекта точек не содержит)
path = os.path.join(out_dir, 'ChartsOfAccounts', chart_name + '.xml')
if not os.path.isfile(path):
std_attr_tail_hint = ("ChartOfAccounts '%s' not found in dump — ExtDimension pairs not generated "
"(platform will add them on load)" % chart_name)
return []
with open(path, encoding='utf-8-sig') as f:
m = re.search(r'<MaxExtDimensionCount>(\d+)</MaxExtDimensionCount>', f.read())
n = int(m.group(1)) if m else 0
out = []
for i in range(1, n + 1):
# ExtDimensionN связан с Account через LinkByType (LinkItem = номер), ExtDimensionTypeN — нет.
out.append(('ExtDimension%d' % i,
{'LinkByType': {'dataPath': 'AccountingRegister.%s.StandardAttribute.Account' % object_name,
'linkItem': i}}))
out.append(('ExtDimensionType%d' % i, {}))
return out
std_attr_tail_derived = {
'AccountingRegister': _acc_register_ext_dimension_tail,
}
def emit_standard_attributes(indent, object_type): def emit_standard_attributes(indent, object_type):
attrs = standard_attributes_by_type.get(object_type) attrs = standard_attributes_by_type.get(object_type)
if not attrs: if not attrs:
@@ -1409,12 +1626,45 @@ def emit_standard_attributes(indent, object_type):
if isinstance(sa, str) and sa == '': if isinstance(sa, str) and sa == '':
return # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок) return # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок)
profile = std_attr_profile.get(object_type, {}) profile = std_attr_profile.get(object_type, {})
# Доп. (опциональные) стандартные реквизиты вне фикс-списка — напр. ExchangeDate у части ПланОбмена # Список типа задаёт ПОРЯДОК всех известных стандартных реквизитов, включая условные:
# (легаси, присутствие не выводится). Эмитим по факту ключа в DSL, ПЕРЕД фикс-списком (их позиция). # их позиция бывает и до, и после обязательных (у бухрегистра PeriodAdjustment идёт
extra = [k for k in sa if k not in attrs] if isinstance(sa, dict) else [] # перед Account, RecordType — после, а ExtDimension1..3/ExtDimensionType1..3 — после Period),
# поэтому «условные скопом вперёд» не выражает канон.
cond = std_attr_conditions.get(object_type)
# Ключи, которых нет в списке типа ВООБЩЕ. По умолчанию их позиция — ПЕРЕД списком
# (легаси вроде ExchangeDate у части планов обмена). Подходящие под хвостовой шаблон
# типа идут ПОСЛЕ, в порядке номера, а внутри номера — сначала ExtDimensionN, затем
# ExtDimensionTypeN (порядок платформы).
tail_re = std_attr_tail_pattern.get(object_type)
extra, tail = [], []
if isinstance(sa, dict):
for k in sa:
if k in attrs:
continue
if tail_re and re.match(tail_re, k):
tail.append(k)
else:
extra.append(k)
# Хвост, выведенный из связанного объекта: дополняет DSL, а не заменяет его — лишнее из DSL
# остаётся (прощаем), недостающее добавляется вместе со своими значениями по умолчанию.
derived_ov = {}
gen = std_attr_tail_derived.get(object_type)
if gen:
for name, dov in gen(defn, obj_name, output_dir):
derived_ov[name] = dov
if name not in tail:
tail.append(name)
tail.sort(key=lambda k: (int(re.search(r'\d+', k).group()), 1 if re.search(r'Type\d+$', k) else 0))
X(f'{indent}<StandardAttributes>') X(f'{indent}<StandardAttributes>')
for a in extra + list(attrs): for a in extra + list(attrs) + tail:
# Условный реквизит: эмитим, если так велят свойства объекта ЛИБО если ключ есть в DSL.
# Дизъюнкция страхует роундтрип — декомпилятор перечисляет все имена блока.
if cond and a in cond:
present = (isinstance(sa, dict) and a in sa) or cond[a](defn)
if not present:
continue
ov = dict(profile.get(a, {})) ov = dict(profile.get(a, {}))
ov.update(derived_ov.get(a, {}))
if isinstance(sa, dict): if isinstance(sa, dict):
d = sa.get(a) d = sa.get(a)
if d: if d:
@@ -2016,8 +2266,13 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
X(f'{indent}\t\t<ExtDimensionAccountingFlag>{esc_xml_text(str(parsed["extDimensionAccountingFlag"]))}</ExtDimensionAccountingFlag>') X(f'{indent}\t\t<ExtDimensionAccountingFlag>{esc_xml_text(str(parsed["extDimensionAccountingFlag"]))}</ExtDimensionAccountingFlag>')
else: else:
X(f'{indent}\t\t<ExtDimensionAccountingFlag/>') X(f'{indent}\t\t<ExtDimensionAccountingFlag/>')
# Use — у реквизитов справочника и ПВХ. Позиция РАЗНАЯ: справочник пишет Use ПЕРЕД
# Indexing, ПВХ — ПОСЛЕ него (корпус acc+erp: Catalog `Use,Indexing,FullTextSearch`,
# ПВХ `Indexing,Use,FullTextSearch,DataHistory`). Отсюда отдельный контекст 'cct':
# структурно реквизит ПВХ совпадает со справочником, расходится только этим порядком.
use_value = parsed.get("use") or "ForItem"
if context == 'catalog': if context == 'catalog':
X(f'{indent}\t\t<Use>{parsed.get("use") or "ForItem"}</Use>') X(f'{indent}\t\t<Use>{use_value}</Use>')
if context not in ('processor', 'processor-tabular'): if context not in ('processor', 'processor-tabular'):
# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>. # Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
if context != 'account-flag': if context != 'account-flag':
@@ -2031,6 +2286,8 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
if parsed.get('indexing'): if parsed.get('indexing'):
indexing = parsed['indexing'] indexing = parsed['indexing']
X(f'{indent}\t\t<Indexing>{indexing}</Indexing>') X(f'{indent}\t\t<Indexing>{indexing}</Indexing>')
if context == 'cct':
X(f'{indent}\t\t<Use>{use_value}</Use>')
# Реквизит адресации задачи: AddressingDimension (между Indexing и FullTextSearch). # Реквизит адресации задачи: AddressingDimension (между Indexing и FullTextSearch).
if context == 'task-addressing' and elem_tag == 'AddressingAttribute': if context == 'task-addressing' and elem_tag == 'AddressingAttribute':
if parsed.get('addressingDimension'): if parsed.get('addressingDimension'):
@@ -2187,6 +2444,10 @@ def emit_enum_value(indent, parsed):
X(f'{indent}\t\t<Comment>{esc_xml_text(parsed["comment"])}</Comment>') X(f'{indent}\t\t<Comment>{esc_xml_text(parsed["comment"])}</Comment>')
else: else:
X(f'{indent}\t\t<Comment/>') X(f'{indent}\t\t<Comment/>')
# Цвет значения перечисления — свойство формата 2.21 (8.5), последним в Properties.
if is_format_221:
color = str(parsed['color']) if parsed.get('color') else 'auto'
X(f'{indent}\t\t<Color>{esc_xml_text(color)}</Color>')
X(f'{indent}\t</Properties>') X(f'{indent}\t</Properties>')
X(f'{indent}</EnumValue>') X(f'{indent}</EnumValue>')
@@ -2811,6 +3072,12 @@ def emit_common_form_properties(indent):
X(f'{i}</UsePurposes>') X(f'{i}</UsePurposes>')
else: else:
X(f'{i}<UsePurposes/>') X(f'{i}<UsePurposes/>')
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
# между UsePurposes и UseStandardCommands.
if is_format_221:
X(f'{i}<UseInInterfaceCompatibilityMode>'
f'{get_enum_prop("UseInInterfaceCompatibilityMode", "useInInterfaceCompatibilityMode", "Any")}'
f'</UseInInterfaceCompatibilityMode>')
use_std_cmds = 'true' if get_bool_prop('useStandardCommands', False) else 'false' use_std_cmds = 'true' if get_bool_prop('useStandardCommands', False) else 'false'
X(f'{i}<UseStandardCommands>{use_std_cmds}</UseStandardCommands>') X(f'{i}<UseStandardCommands>{use_std_cmds}</UseStandardCommands>')
emit_mltext(i, 'ExtendedPresentation', defn.get('extendedPresentation')) emit_mltext(i, 'ExtendedPresentation', defn.get('extendedPresentation'))
@@ -3069,7 +3336,12 @@ def emit_scheduled_job_properties(indent):
else: else:
X(f'{i}<Description/>') X(f'{i}<Description/>')
key = str(defn['key']) if defn.get('key') else '' key = str(defn['key']) if defn.get('key') else ''
X(f'{i}<Key>{esc_xml_text(key)}</Key>') # Пустое значение → самозакрывающийся, как у <Description> выше: Конфигуратор
# не пишет пустых пар.
if key:
X(f'{i}<Key>{esc_xml_text(key)}</Key>')
else:
X(f'{i}<Key/>')
use = 'true' if defn.get('use') is True else 'false' use = 'true' if defn.get('use') is True else 'false'
X(f'{i}<Use>{use}</Use>') X(f'{i}<Use>{use}</Use>')
predefined = 'true' if defn.get('predefined') is True else 'false' predefined = 'true' if defn.get('predefined') is True else 'false'
@@ -3123,6 +3395,9 @@ def emit_report_properties(indent):
emit_verbatim_ref(i, 'DefaultSettingsForm', defn.get('defaultSettingsForm')) emit_verbatim_ref(i, 'DefaultSettingsForm', defn.get('defaultSettingsForm'))
emit_verbatim_ref(i, 'AuxiliarySettingsForm', defn.get('auxiliarySettingsForm')) emit_verbatim_ref(i, 'AuxiliarySettingsForm', defn.get('auxiliarySettingsForm'))
emit_verbatim_ref(i, 'DefaultVariantForm', defn.get('defaultVariantForm')) emit_verbatim_ref(i, 'DefaultVariantForm', defn.get('defaultVariantForm'))
# Вспомогательная форма варианта отчёта — свойство формата 2.21 (8.5).
if is_format_221:
emit_verbatim_ref(i, 'AuxiliaryVariantForm', defn.get('auxiliaryVariantForm'))
emit_verbatim_ref(i, 'VariantsStorage', defn.get('variantsStorage')) emit_verbatim_ref(i, 'VariantsStorage', defn.get('variantsStorage'))
emit_verbatim_ref(i, 'SettingsStorage', defn.get('settingsStorage')) emit_verbatim_ref(i, 'SettingsStorage', defn.get('settingsStorage'))
incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false' incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false'
@@ -3760,7 +4035,8 @@ def emit_web_service_properties(indent):
emit_mltext(i, 'Synonym', synonym) emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>' if defn.get('comment') else f'{i}<Comment/>') X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>' if defn.get('comment') else f'{i}<Comment/>')
namespace = str(defn['namespace']) if defn.get('namespace') else '' namespace = str(defn['namespace']) if defn.get('namespace') else ''
X(f'{i}<Namespace>{esc_xml_text(namespace)}</Namespace>') # Пустое значение → самозакрывающийся, как у <Comment> выше.
X(f'{i}<Namespace>{esc_xml_text(namespace)}</Namespace>' if namespace else f'{i}<Namespace/>')
# XDTOPackages — СПИСОК элементов: ссылка на пакет конфигурации (xr:MDObjectRef) либо URI # XDTOPackages — СПИСОК элементов: ссылка на пакет конфигурации (xr:MDObjectRef) либо URI
# внешнего пространства имён (xs:string). Presentation пуст, CheckState 0 (корпус: 19/19). # внешнего пространства имён (xs:string). Presentation пуст, CheckState 0 (корпус: 19/19).
pkgs = defn.get('xdtoPackages') or [] pkgs = defn.get('xdtoPackages') or []
@@ -3982,6 +4258,16 @@ xmlns_decl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8
def detect_format_version(d): def detect_format_version(d):
while d: while d:
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
ext_path = d + ".xml"
if os.path.isfile(ext_path):
with open(ext_path, "r", encoding="utf-8-sig") as f:
ext_head = f.read(2000)
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
if m:
return m.group(1)
cfg_path = os.path.join(d, "Configuration.xml") cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path): if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f: with open(cfg_path, "r", encoding="utf-8-sig") as f:
@@ -4034,6 +4320,15 @@ compat_mode = detect_compatibility_mode(output_dir)
# глушил бы TypeReductionMode, который платформа этих версий пишет, и роундтрип бы разъезжался. # глушил бы TypeReductionMode, который платформа этих версий пишет, и роундтрип бы разъезжался.
is_format_218 = format_rank(format_version) >= 218 is_format_218 = format_rank(format_version) >= 218
is_format_220 = format_rank(format_version) >= 220 is_format_220 = format_rank(format_version) >= 220
is_format_221 = format_rank(format_version) >= 221
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту.
# Только для шапок MetaDataObject и Form — в файлах с корнем extrnprops
# (Ext/ClientApplicationInterface.xml и т.п.) платформа его не пишет.
if is_format_221:
xmlns_decl = xmlns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
# Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами. # Дефолт длины номера строки ТЧ: с режима 8.3.27 платформа заводит новые ТЧ с 9 разрядами.
line_number_length_default = 9 if compat_mode_rank(compat_mode) >= 80327 else 5 line_number_length_default = 9 if compat_mode_rank(compat_mode) >= 80327 else 5
@@ -4174,7 +4469,7 @@ if obj_type in types_with_attr_ts:
elif obj_type in ('DataProcessor', 'Report'): elif obj_type in ('DataProcessor', 'Report'):
context = 'processor' context = 'processor'
elif obj_type == 'ChartOfCharacteristicTypes': elif obj_type == 'ChartOfCharacteristicTypes':
context = 'catalog' # реквизиты ПВХ структурно как у справочника (Use/FillFromFillingValue/DataHistory) context = 'cct' # как catalog (Use/FillFromFillingValue/DataHistory), но Use ПОСЛЕ Indexing
elif obj_type in ('ChartOfAccounts', 'ChartOfCalculationTypes'): elif obj_type in ('ChartOfAccounts', 'ChartOfCalculationTypes'):
context = 'account' # как catalog, но БЕЗ <Use> (реквизиты ПС/ПВР не иерархичны как справочник) context = 'account' # как catalog, но БЕЗ <Use> (реквизиты ПС/ПВР не иерархичны как справочник)
else: else:
@@ -4245,18 +4540,27 @@ if obj_type in ('InformationRegister', 'AccumulationRegister', 'AccountingRegist
# Все семейства регистров: ресурсы/измерения — через богатый emit_attribute (общий слой object-свойств). # Все семейства регистров: ресурсы/измерения — через богатый emit_attribute (общий слой object-свойств).
dim_res_ctx = {'InformationRegister': 'register-info', 'AccumulationRegister': 'register-accum', dim_res_ctx = {'InformationRegister': 'register-info', 'AccumulationRegister': 'register-accum',
'CalculationRegister': 'register-calc', 'AccountingRegister': 'register-account'}.get(obj_type) 'CalculationRegister': 'register-calc', 'AccountingRegister': 'register-account'}.get(obj_type)
for r in resources: # Порядок видов детей — канон выгрузки, снят с корпуса (acc+erp, разброса внутри
if dim_res_ctx: # типа нет): у большинства регистров Resource, Attribute, Dimension, а у
emit_attribute('\t\t\t', r, dim_res_ctx, 'Resource') # бухгалтерского — Dimension, Resource, Attribute. Команды у платформы идут
# последними, как и здесь.
kind_order = ['dim', 'res', 'attr'] if obj_type == 'AccountingRegister' else ['res', 'attr', 'dim']
for kind in kind_order:
if kind == 'res':
for r in resources:
if dim_res_ctx:
emit_attribute('\t\t\t', r, dim_res_ctx, 'Resource')
else:
emit_resource('\t\t\t', r, obj_type)
elif kind == 'dim':
for d in dims:
if dim_res_ctx:
emit_attribute('\t\t\t', d, dim_res_ctx, 'Dimension')
else:
emit_dimension('\t\t\t', d, obj_type)
else: else:
emit_resource('\t\t\t', r, obj_type) for a in reg_attrs:
for d in dims: emit_attribute('\t\t\t', a, reg_ctx)
if dim_res_ctx:
emit_attribute('\t\t\t', d, dim_res_ctx, 'Dimension')
else:
emit_dimension('\t\t\t', d, obj_type)
for a in reg_attrs:
emit_attribute('\t\t\t', a, reg_ctx)
for cmd in reg_commands: for cmd in reg_commands:
emit_command('\t\t\t', cmd['name'], cmd['def']) emit_command('\t\t\t', cmd['name'], cmd['def'])
X('\t\t</ChildObjects>') X('\t\t</ChildObjects>')
@@ -4360,7 +4664,7 @@ if obj_type == 'WebService':
X(f'\t</{obj_type}>') X(f'\t</{obj_type}>')
X('</MetaDataObject>') X('</MetaDataObject>')
metadata_xml = '\n'.join(lines) + '\n' metadata_xml = '\r\n'.join(lines)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 16. Write files # 16. Write files
@@ -4422,7 +4726,7 @@ os.makedirs(type_dir, exist_ok=True)
if obj_type not in types_no_sub_dir: if obj_type not in types_no_sub_dir:
os.makedirs(obj_sub_dir, exist_ok=True) os.makedirs(obj_sub_dir, exist_ok=True)
write_utf8_bom(main_xml_path, metadata_xml) write_xml_file_keep_eol(main_xml_path, metadata_xml)
# Module files # Module files
modules_created = [] modules_created = []
@@ -4498,10 +4802,14 @@ if obj_type == 'CommonForm':
'xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" ' 'xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" '
'xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" ' 'xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"') 'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
cf_form_xml = ('<?xml version="1.0" encoding="UTF-8"?>\n<Form ' + cf_ns + ' version="' + format_version + '">\n' # Шапка Form на 2.21 тоже несёт палитру — см. комментарий у xmlns_decl.
'\t<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">\n\t\t<Autofill>true</Autofill>\n\t</AutoCommandBar>\n' if is_format_221:
'\t<ChildItems/>\n</Form>\n') cf_ns = cf_ns.replace(' xmlns:style=',
write_utf8_bom(cf_form_xml_path, cf_form_xml) ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
cf_form_xml = ('<?xml version="1.0" encoding="UTF-8"?>\r\n<Form ' + cf_ns + ' version="' + format_version + '">\r\n'
'\t<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">\r\n\t\t<Autofill>true</Autofill>\r\n\t</AutoCommandBar>\r\n'
'\t<ChildItems/>\r\n</Form>\r\n')
write_xml_file_keep_eol(cf_form_xml_path, cf_form_xml)
modules_created.append(cf_form_xml_path) modules_created.append(cf_form_xml_path)
cf_module_dir = os.path.join(ext_dir, 'Form') cf_module_dir = os.path.join(ext_dir, 'Form')
os.makedirs(cf_module_dir, exist_ok=True) os.makedirs(cf_module_dir, exist_ok=True)
@@ -4589,10 +4897,18 @@ def emit_predef_item(out, val, indent, code_type):
def build_predefined_xml(items, xsi_type, code_type): def build_predefined_xml(items, xsi_type, code_type):
out = ['<?xml version="1.0" encoding="UTF-8"?>'] out = ['<?xml version="1.0" encoding="UTF-8"?>']
out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="{xsi_type}" version="{format_version}">') out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="{xsi_type}" version="{format_version}">')
for it in items: # Шапка Predefined.xml не объявляет cfg (predef/v8/xr/xs/xsi) — на время сборки этого
emit_predef_item(out, it, '\t', code_type) # файла ссылочный тип уходит на локальную форму, как делает и платформа.
global cfg_prefix
saved_cfg_prefix = cfg_prefix
cfg_prefix = None
try:
for it in items:
emit_predef_item(out, it, '\t', code_type)
finally:
cfg_prefix = saved_cfg_prefix
out.append('</PredefinedData>') out.append('</PredefinedData>')
return '\n'.join(out) + '\n' return '\r\n'.join(out)
# --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/ # --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/
# ExtDimensionTypes/ChildItems). Флаги перечисляем по def-порядку признаков плана; в DSL — только TRUE. --- # ExtDimensionTypes/ChildItems). Флаги перечисляем по def-порядку признаков плана; в DSL — только TRUE. ---
@@ -4687,10 +5003,17 @@ def emit_predef_account(out, val, indent, obj_nm, acct_flag_names, ext_dim_flag_
def build_predefined_account_xml(items, obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref=''): def build_predefined_account_xml(items, obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref=''):
out = ['<?xml version="1.0" encoding="UTF-8"?>'] out = ['<?xml version="1.0" encoding="UTF-8"?>']
out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ChartOfAccountsPredefinedItems" version="{format_version}">') out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ChartOfAccountsPredefinedItems" version="{format_version}">')
for it in items: # См. build_predefined_xml: шапка этого файла cfg не объявляет.
emit_predef_account(out, it, '\t', obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref) global cfg_prefix
saved_cfg_prefix = cfg_prefix
cfg_prefix = None
try:
for it in items:
emit_predef_account(out, it, '\t', obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref)
finally:
cfg_prefix = saved_cfg_prefix
out.append('</PredefinedData>') out.append('</PredefinedData>')
return '\n'.join(out) + '\n' return '\r\n'.join(out)
# Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase). # Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase).
def emit_predef_calc_type(out, val, indent): def emit_predef_calc_type(out, val, indent):
@@ -4712,7 +5035,7 @@ def build_predefined_calc_type_xml(items):
for it in items: for it in items:
emit_predef_calc_type(out, it, '\t') emit_predef_calc_type(out, it, '\t')
out.append('</PredefinedData>') out.append('</PredefinedData>')
return '\n'.join(out) + '\n' return '\r\n'.join(out)
# Special files # Special files
# --- Состав плана обмена (ExchangePlan, Ext/Content.xml). Ключ `content`/`Состав`: # --- Состав плана обмена (ExchangePlan, Ext/Content.xml). Ключ `content`/`Состав`:
@@ -4766,7 +5089,7 @@ if obj_type == 'ExchangePlan':
parts.append(f'\t\t<AutoRecord>{it["autoRecord"]}</AutoRecord>\r\n') parts.append(f'\t\t<AutoRecord>{it["autoRecord"]}</AutoRecord>\r\n')
parts.append('\t</Item>\r\n') parts.append('\t</Item>\r\n')
parts.append('</ExchangePlanContent>\r\n') parts.append('</ExchangePlanContent>\r\n')
write_utf8_bom(content_path, ''.join(parts)) write_xml_file_keep_eol(content_path, ''.join(parts))
modules_created.append(content_path) modules_created.append(content_path)
elif not os.path.isfile(content_path): elif not os.path.isfile(content_path):
# При пустом составе платформа всё равно пишет пустой <ExchangePlanContent/> — проверено на # При пустом составе платформа всё равно пишет пустой <ExchangePlanContent/> — проверено на
@@ -4774,7 +5097,7 @@ if obj_type == 'ExchangePlan':
# (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.) # (Единственный план обмена БЕЗ файла найден в УТ — хвостовая аномалия конфигурации, не правило.)
ensure_ext_dir() ensure_ext_dir()
content_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent {xep_ns} version="{format_version}"/>\r\n' content_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent {xep_ns} version="{format_version}"/>\r\n'
write_utf8_bom(content_path, content_xml) write_xml_file_keep_eol(content_path, content_xml)
modules_created.append(content_path) modules_created.append(content_path)
if obj_type == 'BusinessProcess': if obj_type == 'BusinessProcess':
@@ -4782,7 +5105,7 @@ if obj_type == 'BusinessProcess':
if not os.path.isfile(flowchart_path): if not os.path.isfile(flowchart_path):
ensure_ext_dir() ensure_ext_dir()
flowchart_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses" version="{format_version}"/>\r\n' flowchart_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses" version="{format_version}"/>\r\n'
write_utf8_bom(flowchart_path, flowchart_xml) write_xml_file_keep_eol(flowchart_path, flowchart_xml)
modules_created.append(flowchart_path) modules_created.append(flowchart_path)
# Предопределённые элементы (Ext/Predefined.xml). Root-элемент по типу. # Предопределённые элементы (Ext/Predefined.xml). Root-элемент по типу.
@@ -4795,20 +5118,20 @@ if obj_type == 'ChartOfAccounts' and defn.get('predefined'):
edt_ref = resolve_type_prefix_syn(str(defn['extDimensionTypes'])) if defn.get('extDimensionTypes') else '' edt_ref = resolve_type_prefix_syn(str(defn['extDimensionTypes'])) if defn.get('extDimensionTypes') else ''
predef_xml = build_predefined_account_xml(defn['predefined'], obj_name, af_names, edf_names, edt_ref) predef_xml = build_predefined_account_xml(defn['predefined'], obj_name, af_names, edf_names, edt_ref)
predef_path = os.path.join(ext_dir, 'Predefined.xml') predef_path = os.path.join(ext_dir, 'Predefined.xml')
write_utf8_bom(predef_path, predef_xml) write_xml_file_keep_eol(predef_path, predef_xml)
modules_created.append(predef_path) modules_created.append(predef_path)
elif obj_type == 'ChartOfCalculationTypes' and defn.get('predefined'): elif obj_type == 'ChartOfCalculationTypes' and defn.get('predefined'):
ensure_ext_dir() ensure_ext_dir()
predef_xml = build_predefined_calc_type_xml(defn['predefined']) predef_xml = build_predefined_calc_type_xml(defn['predefined'])
predef_path = os.path.join(ext_dir, 'Predefined.xml') predef_path = os.path.join(ext_dir, 'Predefined.xml')
write_utf8_bom(predef_path, predef_xml) write_xml_file_keep_eol(predef_path, predef_xml)
modules_created.append(predef_path) modules_created.append(predef_path)
elif obj_type in predef_root_by_type and defn.get('predefined'): elif obj_type in predef_root_by_type and defn.get('predefined'):
ensure_ext_dir() ensure_ext_dir()
cat_code_type = str(defn['codeType']) if defn.get('codeType') else 'String' cat_code_type = str(defn['codeType']) if defn.get('codeType') else 'String'
predef_xml = build_predefined_xml(defn['predefined'], predef_root_by_type[obj_type], cat_code_type) predef_xml = build_predefined_xml(defn['predefined'], predef_root_by_type[obj_type], cat_code_type)
predef_path = os.path.join(ext_dir, 'Predefined.xml') predef_path = os.path.join(ext_dir, 'Predefined.xml')
write_utf8_bom(predef_path, predef_xml) write_xml_file_keep_eol(predef_path, predef_xml)
modules_created.append(predef_path) modules_created.append(predef_path)
# Модули команд (Commands/<Имя>/Ext/CommandModule.bsl) — заготовка обработчика. # Модули команд (Commands/<Имя>/Ext/CommandModule.bsl) — заготовка обработчика.
@@ -4949,6 +5272,8 @@ elif reg_result == 'no-config':
print(f' Configuration.xml: not found at {config_xml_path} (register manually)') print(f' Configuration.xml: not found at {config_xml_path} (register manually)')
# Cross-reference hints # Cross-reference hints
if std_attr_tail_hint:
print(f'[HINT] {std_attr_tail_hint}')
if obj_type == 'AccountingRegister' and not defn.get('chartOfAccounts'): if obj_type == 'AccountingRegister' and not defn.get('chartOfAccounts'):
print('[HINT] AccountingRegister requires ChartOfAccounts reference:') print('[HINT] AccountingRegister requires ChartOfAccounts reference:')
print(' /meta-edit -Operation modify-property -Value "ChartOfAccounts=ChartOfAccounts.XXX"') print(' /meta-edit -Operation modify-property -Value "ChartOfAccounts=ChartOfAccounts.XXX"')
@@ -1,4 +1,4 @@
# meta-decompile v0.63 — XML объекта метаданных 1С → JSON-черновик формата meta-compile # meta-decompile v0.64 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# #
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document, # Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# meta-decompile v0.63 — XML объекта метаданных 1С → JSON-черновик формата meta-compile # meta-decompile v0.64 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# #
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии. # Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
@@ -20,6 +20,28 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Namespaces (зеркало XmlNamespaceManager) --- # --- Namespaces (зеркало XmlNamespaceManager) ---
NS_MD = "http://v8.1c.ru/8.3/MDClasses" NS_MD = "http://v8.1c.ru/8.3/MDClasses"
NS_V8 = "http://v8.1c.ru/8.1/data/core" NS_V8 = "http://v8.1c.ru/8.1/data/core"
@@ -2099,7 +2121,7 @@ def main():
parser = argparse.ArgumentParser(description='Decompile 1C metadata object XML to JSON DSL (draft)', allow_abbrev=False) parser = argparse.ArgumentParser(description='Decompile 1C metadata object XML to JSON DSL (draft)', allow_abbrev=False)
parser.add_argument('-ObjectPath', '-Path', dest='ObjectPath', type=str, required=True) parser.add_argument('-ObjectPath', '-Path', dest='ObjectPath', type=str, required=True)
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None) parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
args = parser.parse_args() args = ci_parse_args(parser)
object_path = args.ObjectPath object_path = args.ObjectPath
if not os.path.exists(object_path): if not os.path.exists(object_path):
+1 -2
View File
@@ -132,8 +132,7 @@ JSON — строки и/или объекты (для групп с вложе
**Свойства** задавайте по имени свойства 1С (PascalCase, как в конфигураторе): `Indexing`, `FillChecking`, **Свойства** задавайте по имени свойства 1С (PascalCase, как в конфигураторе): `Indexing`, `FillChecking`,
`Use`, `FullTextSearch`, `DataHistory`, `PasswordMode`, `MultiLine`, `Mask`, `CreateOnInput`, `QuickChoice` и др. `Use`, `FullTextSearch`, `DataHistory`, `PasswordMode`, `MultiLine`, `Mask`, `CreateOnInput`, `QuickChoice` и др.
Свойство можно задать, даже если у реквизита оно ещё не выставлено. Опечатка в имени свойства → ошибка Свойство можно задать, даже если у реквизита оно ещё не выставлено.
(правка не теряется молча).
```powershell ```powershell
-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)" -Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)"
@@ -12,7 +12,7 @@
``` ```
Свойство можно задать, даже если оно ещё не выставлено у объекта (например `FullTextSearch`, `DataHistory`). Свойство можно задать, даже если оно ещё не выставлено у объекта (например `FullTextSearch`, `DataHistory`).
Опечатка в имени свойства → ошибка (правка не теряется молча). Допустимы имена свойств соответствующего типа объекта. Допустимы имена свойств соответствующего типа объекта.
### Type — тип значения (Константа, ПВХ) ### Type — тип значения (Константа, ПВХ)
@@ -21,8 +21,7 @@
```powershell ```powershell
-Operation modify-property -Value "Type=String(100) + Number(15,2) + CatalogRef.Номенклатура" -Operation modify-property -Value "Type=String(100) + Number(15,2) + CatalogRef.Номенклатура"
``` ```
Структурные свойства (со вложенными узлами) в скалярный текст не превращаются: попытка задать Структурные свойства (со вложенными узлами) через `Ключ=Значение` не задаются — исключение только `Type`.
такое свойство обычным `Ключ=Значение` (кроме `Type`) завершается ошибкой до записи файла.
## Свойства-списки ## Свойства-списки
+71 -10
View File
@@ -1,4 +1,4 @@
# meta-edit v1.24 — Edit existing 1C metadata object XML # meta-edit v1.38 — Edit existing 1C metadata object XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[string]$DefinitionFile, [string]$DefinitionFile,
@@ -89,7 +89,7 @@ $script:validEnumValues = @{
"RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff") "RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff")
"RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll") "RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll")
"ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession")
"ReuseSessions" = @("DontUse","AutoUse") "ReuseSessions" = @("DontUse","Use","AutoUse")
"FillChecking" = @("DontCheck","ShowError","ShowWarning") "FillChecking" = @("DontCheck","ShowError","ShowWarning")
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
} }
@@ -313,6 +313,23 @@ function Info($msg) {
# ============================================================ # ============================================================
$root = $script:xmlDoc.DocumentElement $root = $script:xmlDoc.DocumentElement
# Префикс пространства current-config, объявленный в КОРНЕ файла (у платформы — cfg).
# Ищем по объявлениям корня, а не через GetPrefixOfNamespace: ссылочный тип живёт в
# ТЕКСТЕ узла, поэтому XML-слой этот префикс не отслеживает. $null = корень URI не
# объявляет → эмиттер остаётся на самодостаточной локальной форме.
# Версия формата правимого файла — из его же корня. Нужна эмиттерам: часть свойств
# появилась в поздних версиях (напр. <Color> у значения перечисления — в 2.21).
$script:formatVersion = $root.GetAttribute("version")
if (-not $script:formatVersion) { $script:formatVersion = "2.17" }
$script:isFormat221 = ($script:formatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221
$script:cfgUri = 'http://v8.1c.ru/8.1/data/enterprise/current-config'
$script:cfgPrefix = $null
foreach ($a in $root.Attributes) {
if ($a.Prefix -eq 'xmlns' -and $a.Value -eq $script:cfgUri) { $script:cfgPrefix = $a.LocalName; break }
}
if ($root.LocalName -ne "MetaDataObject") { if ($root.LocalName -ne "MetaDataObject") {
Write-Error "Root element must be MetaDataObject, got: $($root.LocalName)" Write-Error "Root element must be MetaDataObject, got: $($root.LocalName)"
exit 1 exit 1
@@ -444,7 +461,20 @@ function Resolve-TypeStr {
param([string]$typeStr) param([string]$typeStr)
if (-not $typeStr) { return $typeStr } if (-not $typeStr) { return $typeStr }
# Parameterized: Number(15,2), Строка(100) # Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
if ($typeStr.StartsWith('cfg:')) {
$typeStr = $typeStr.Substring(4)
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
}
# Параметризованные типы: Number(15,2), Строка(100)
if ($typeStr -match '^([^(]+)\((.+)\)$') { if ($typeStr -match '^([^(]+)\((.+)\)$') {
$baseName = $Matches[1].Trim() $baseName = $Matches[1].Trim()
$params = $Matches[2] $params = $Matches[2]
@@ -453,17 +483,17 @@ function Resolve-TypeStr {
return $typeStr return $typeStr
} }
# Reference: СправочникСсылка.Организации # Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
if ($typeStr.Contains('.')) { if ($typeStr.Contains('.')) {
$dotIdx = $typeStr.IndexOf('.') $dotIdx = $typeStr.IndexOf('.')
$prefix = $typeStr.Substring(0, $dotIdx) $prefix = $typeStr.Substring(0, $dotIdx)
$suffix = $typeStr.Substring($dotIdx) $suffix = $typeStr.Substring($dotIdx) # includes the dot
$resolved = $script:typeSynonyms[$prefix.ToLower()] $resolved = $script:typeSynonyms[$prefix.ToLower()]
if ($resolved) { return "$resolved$suffix" } if ($resolved) { return "$resolved$suffix" }
return $typeStr return $typeStr
} }
# Simple # Простое имя
$resolved = $script:typeSynonyms[$typeStr.ToLower()] $resolved = $script:typeSynonyms[$typeStr.ToLower()]
if ($resolved) { return $resolved } if ($resolved) { return $resolved }
return $typeStr return $typeStr
@@ -557,9 +587,19 @@ function Build-TypeContentXml {
return $sb.ToString().TrimEnd("`r","`n") return $sb.ToString().TrimEnd("`r","`n")
} }
# Reference types — use local xmlns declaration for 1C compatibility # Ссылочные типы — префиксом, объявленным в КОРНЕ файла (у платформы это cfg).
# Раньше здесь всегда объявлялся локальный xmlns:d5p1 на тот же URI, что уже есть
# в шапке: платформа принимала, но при цикле «загрузить в базу → выгрузить»
# переписывала каждый ссылочный тип в cfg: — diff-шум на ровном месте.
# Если корень URI не объявляет (файл не от платформы), остаёмся на самодостаточной
# локальной форме: префикс тут — ТЕКСТ узла, XML-слой про него не знает и сам
# объявление не добавит, так что иначе получился бы неразрешимый префикс.
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') { if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') {
$sb.AppendLine("$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$typeStr</v8:Type>") | Out-Null if ($script:cfgPrefix) {
$sb.AppendLine("$indent<v8:Type>$($script:cfgPrefix):$typeStr</v8:Type>") | Out-Null
} else {
$sb.AppendLine("$indent<v8:Type xmlns:d5p1=`"$script:cfgUri`">d5p1:$typeStr</v8:Type>") | Out-Null
}
return $sb.ToString().TrimEnd("`r","`n") return $sb.ToString().TrimEnd("`r","`n")
} }
@@ -1279,6 +1319,9 @@ function Build-EnumValueFragment {
$sb.AppendLine("$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>") | Out-Null $sb.AppendLine("$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>") | Out-Null
$sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null
$sb.AppendLine("$indent`t`t<Comment/>") | Out-Null $sb.AppendLine("$indent`t`t<Comment/>") | Out-Null
# Цвет значения — свойство формата 2.21 (8.5). Без него добавленное значение
# отличалось бы от соседних, написанных платформой.
if ($script:isFormat221) { $sb.AppendLine("$indent`t`t<Color>auto</Color>") | Out-Null }
$sb.AppendLine("$indent`t</Properties>") | Out-Null $sb.AppendLine("$indent`t</Properties>") | Out-Null
$sb.Append("$indent</EnumValue>") | Out-Null $sb.Append("$indent</EnumValue>") | Out-Null
return $sb.ToString() return $sb.ToString()
@@ -2067,6 +2110,11 @@ function Modify-Properties($propsDef) {
exit 1 exit 1
} }
# Значение свойства-перечисления приводим к канону (как это делает meta-compile): иначе
# в XML уезжает то, что дала модель, и платформа отвергает выгрузку уже при загрузке.
# Неизвестное свойство функция пропускает как есть, неизвестное значение — отвергает.
$valueStr = Normalize-EnumValue $propName $valueStr
$propEl.InnerText = $valueStr $propEl.InnerText = $valueStr
Info "Modified property: $propName = $valueStr" Info "Modified property: $propName = $valueStr"
$script:modifyCount++ $script:modifyCount++
@@ -2811,7 +2859,11 @@ function Build-ChoiceParametersXml([string]$indent, $cp) {
$script:fillBoolTrue = @('true','истина','да') $script:fillBoolTrue = @('true','истина','да')
$script:fillBoolFalse = @('false','ложь','нет') $script:fillBoolFalse = @('false','ложь','нет')
function Esc-XmlText([string]$s) { return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;') } function Esc-XmlText {
param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
function Get-FillTypeCategory([string]$typeStr) { function Get-FillTypeCategory([string]$typeStr) {
if (-not $typeStr) { return 'String' } if (-not $typeStr) { return 'String' }
@@ -3138,7 +3190,8 @@ function Add-PredefinedItems($items) {
$hdr = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"$xsiType`" version=`"$version`">`r`n" $hdr = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<PredefinedData xmlns=`"http://v8.1c.ru/8.3/xcf/predef`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"$xsiType`" version=`"$version`">`r`n"
$text = "$hdr$itemsXml</PredefinedData>`r`n" $text = "$hdr$itemsXml</PredefinedData>`r`n"
} }
[System.IO.File]::WriteAllText($path, $text, $utf8Bom) # Создаваемый файл — по канону: без перевода строки в конце.
[System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $utf8Bom)
$n = @($items).Count $n = @($items).Count
Info "Added $n predefined item(s) → $path" Info "Added $n predefined item(s) → $path"
$script:addCount += $n $script:addCount += $n
@@ -3208,9 +3261,17 @@ if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) {
$text = $text.Substring(1) $text = $text.Substring(1)
} }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') $text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Write with BOM # Write with BOM
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $utf8Bom = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) [System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
Info "Saved: $resolvedPath" Info "Saved: $resolvedPath"
+146 -39
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# meta-edit v1.24 — Edit existing 1C metadata object XML # meta-edit v1.38 — Edit existing 1C metadata object XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -11,6 +11,65 @@ import sys
import uuid import uuid
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================ # ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
@@ -198,6 +257,12 @@ V8_NS = "http://v8.1c.ru/8.1/data/core"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
XS_NS = "http://www.w3.org/2001/XMLSchema" XS_NS = "http://www.w3.org/2001/XMLSchema"
CFG_NS = "http://v8.1c.ru/8.1/data/enterprise/current-config" CFG_NS = "http://v8.1c.ru/8.1/data/enterprise/current-config"
# Версия формата правимого файла — из его же корня. Нужна эмиттерам: часть свойств
# появилась в поздних версиях (напр. <Color> у значения перечисления — в 2.21).
is_format_221 = False
# Префикс current-config, объявленный в КОРНЕ правимого файла (у платформы — cfg).
# None = корень его не объявляет → эмиттер ссылочных типов остаётся на локальной форме.
cfg_prefix = None
NSMAP_WRAPPER = { NSMAP_WRAPPER = {
None: MD_NS, None: MD_NS,
@@ -252,7 +317,8 @@ def localname(el):
def esc_xml(s): def esc_xml(s):
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;") # Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
# ============================================================ # ============================================================
@@ -291,6 +357,9 @@ enum_value_aliases = {
'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder', 'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder',
} }
# Словарь ищет ПОЛЬЗОВАТЕЛЬСКИЙ ввод — поиск регистронезависим, как хеш-таблица PS1.
enum_value_aliases = CIDict(enum_value_aliases)
valid_enum_values = { valid_enum_values = {
'RegisterType': ['Balance', 'Turnovers'], 'RegisterType': ['Balance', 'Turnovers'],
'WriteMode': ['Independent', 'RecorderSubordinate'], 'WriteMode': ['Independent', 'RecorderSubordinate'],
@@ -311,7 +380,7 @@ valid_enum_values = {
'RegisterRecordsDeletion': ['AutoDelete', 'AutoDeleteOnUnpost', 'AutoDeleteOff'], 'RegisterRecordsDeletion': ['AutoDelete', 'AutoDeleteOnUnpost', 'AutoDeleteOff'],
'RegisterRecordsWritingOnPost': ['WriteModified', 'WriteSelected', 'WriteAll'], 'RegisterRecordsWritingOnPost': ['WriteModified', 'WriteSelected', 'WriteAll'],
'ReturnValuesReuse': ['DontUse', 'DuringRequest', 'DuringSession'], 'ReturnValuesReuse': ['DontUse', 'DuringRequest', 'DuringSession'],
'ReuseSessions': ['DontUse', 'AutoUse'], 'ReuseSessions': ['DontUse', 'Use', 'AutoUse'],
'FillChecking': ['DontCheck', 'ShowError', 'ShowWarning'], 'FillChecking': ['DontCheck', 'ShowError', 'ShowWarning'],
'Indexing': ['DontIndex', 'Index', 'IndexWithAdditionalOrder'], 'Indexing': ['DontIndex', 'Index', 'IndexWithAdditionalOrder'],
} }
@@ -410,37 +479,48 @@ type_synonyms = {
# ============================================================ # ============================================================
# Алиас на локальный словарь: тело resolve_type_str ниже — общая реализация,
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
TYPE_SYNONYMS = type_synonyms
def resolve_type_str(type_str): def resolve_type_str(type_str):
if not type_str: if not type_str:
return type_str return type_str
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
# Parameterized: Number(15,2), Строка(100) # поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
m = re.match(r"^([^(]+)\((.+)\)$", type_str) # «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
if type_str.startswith('cfg:'):
type_str = type_str[4:]
elif '.' in type_str and re.match(r'^d\d+p\d+:', type_str):
type_str = type_str[type_str.index(':') + 1:]
# Параметризованные типы: Number(15,2), Строка(100)
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
if m: if m:
base_name = m.group(1).strip() base_name = m.group(1).strip()
params = m.group(2) params = m.group(2)
resolved = type_synonyms.get(base_name.lower()) resolved = TYPE_SYNONYMS.get(base_name.lower())
if resolved: if resolved:
return f"{resolved}({params})" return f'{resolved}({params})'
return type_str return type_str
# Ссылочные типы: СправочникСсылка.Организации -> CatalogRef.Организации
# Reference: СправочникСсылка.Организации if '.' in type_str:
if "." in type_str: dot_idx = type_str.index('.')
dot_idx = type_str.index(".")
prefix = type_str[:dot_idx] prefix = type_str[:dot_idx]
suffix = type_str[dot_idx:] suffix = type_str[dot_idx:] # includes the dot
resolved = type_synonyms.get(prefix.lower()) resolved = TYPE_SYNONYMS.get(prefix.lower())
if resolved: if resolved:
return f"{resolved}{suffix}" return f'{resolved}{suffix}'
return type_str return type_str
# Простое имя
# Simple resolved = TYPE_SYNONYMS.get(type_str.lower())
resolved = type_synonyms.get(type_str.lower())
if resolved: if resolved:
return resolved return resolved
return type_str return type_str
def build_type_content_xml(indent, type_str): def build_type_content_xml(indent, type_str):
if not type_str: if not type_str:
return "" return ""
@@ -525,14 +605,23 @@ def build_type_content_xml(indent, type_str):
lines.append(f"{indent}<v8:TypeSet>cfg:DefinedType.{dt_name}</v8:TypeSet>") lines.append(f"{indent}<v8:TypeSet>cfg:DefinedType.{dt_name}</v8:TypeSet>")
return "\r\n".join(lines) return "\r\n".join(lines)
# Reference types — use local xmlns declaration for 1C compatibility # Ссылочные типы — префиксом, объявленным в КОРНЕ файла (у платформы это cfg).
# Раньше здесь всегда объявлялся локальный xmlns:d5p1 на тот же URI, что уже есть
# в шапке: платформа принимала, но при цикле «загрузить в базу → выгрузить»
# переписывала каждый ссылочный тип в cfg: — diff-шум на ровном месте.
# Если корень URI не объявляет (файл не от платформы), остаёмся на самодостаточной
# локальной форме: префикс тут — ТЕКСТ узла, XML-слой про него не знает и сам
# объявление не добавит, так что иначе получился бы неразрешимый префикс.
m = re.match( m = re.match(
r"^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|" r"^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|"
r"ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$", r"ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$",
type_str, type_str,
) )
if m: if m:
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{type_str}</v8:Type>') if cfg_prefix:
lines.append(f'{indent}<v8:Type>{cfg_prefix}:{type_str}</v8:Type>')
else:
lines.append(f'{indent}<v8:Type xmlns:d5p1="{CFG_NS}">d5p1:{type_str}</v8:Type>')
return "\r\n".join(lines) return "\r\n".join(lines)
# Fallback # Fallback
@@ -1254,6 +1343,10 @@ def build_enum_value_fragment(parsed, indent):
lines.append(f"{indent}\t\t<Name>{esc_xml_text(parsed['name'])}</Name>") lines.append(f"{indent}\t\t<Name>{esc_xml_text(parsed['name'])}</Name>")
lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", parsed["synonym"])) lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", parsed["synonym"]))
lines.append(f"{indent}\t\t<Comment/>") lines.append(f"{indent}\t\t<Comment/>")
# Цвет значения — свойство формата 2.21 (8.5). Без него добавленное значение
# отличалось бы от соседних, написанных платформой.
if is_format_221:
lines.append(f"{indent}\t\t<Color>auto</Color>")
lines.append(f"{indent}\t</Properties>") lines.append(f"{indent}\t</Properties>")
lines.append(f"{indent}</EnumValue>") lines.append(f"{indent}</EnumValue>")
return "\r\n".join(lines) return "\r\n".join(lines)
@@ -1936,6 +2029,11 @@ def modify_properties(props_def):
print(f"meta-edit: modify-property: свойство '{prop_name}' структурное (содержит дочерние узлы) — установка скалярного текста повредит XML; не поддерживается", file=sys.stderr) print(f"meta-edit: modify-property: свойство '{prop_name}' структурное (содержит дочерние узлы) — установка скалярного текста повредит XML; не поддерживается", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Значение свойства-перечисления приводим к канону (как это делает meta-compile): иначе
# в XML уезжает то, что дала модель, и платформа отвергает выгрузку уже при загрузке.
# Неизвестное свойство функция пропускает как есть, неизвестное значение — отвергает.
value_str = normalize_enum_value(prop_name, value_str)
# Set inner text — clear children first, set text # Set inner text — clear children first, set text
for ch in list(prop_el): for ch in list(prop_el):
prop_el.remove(ch) prop_el.remove(ch)
@@ -2962,21 +3060,22 @@ def _detect_xml_style(path):
def _finalize_xml_bytes(xml_bytes, style): def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None).""" """Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
enc_decl = style["enc"] if style else "utf-8" выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace( xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>", b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>') b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах) # Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"") xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n")) .replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть) # Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else True want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n") xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl: if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение) # EOL — как в оригинале (новый файл → CRLF, канон #57)
if style and style["crlf"]: if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n") xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes return xml_bytes
@@ -2985,13 +3084,9 @@ def save_xml(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline.""" """Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path) style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
# Fix d5p1 namespace declarations stripped by lxml (it treats them as unused # Костыль, возвращавший xmlns:d5p1 (lxml выбрасывал его как неиспользуемый, ведь
# because d5p1: appears only in text content, not in element/attribute names) # префикс встречается только в тексте узла), удалён вместе с переходом на корневой
xml_bytes = re.sub( # cfg: — объявление теперь берётся из шапки самого файла и не требует починки.
b'(<v8:Type)(?! xmlns:d5p1)(>d5p1:)',
b'\\1 xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config"\\2',
xml_bytes
)
xml_bytes = _finalize_xml_bytes(xml_bytes, style) xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f: with open(path, "wb") as f:
if style is None or style["bom"]: if style is None or style["bom"]:
@@ -3094,7 +3189,9 @@ def add_predefined_items(items):
item_list = items if isinstance(items, list) else [items] item_list = items if isinstance(items, list) else [items]
items_xml = ''.join(build_predef_item_xml('\t', it, code_type) for it in item_list) items_xml = ''.join(build_predef_item_xml('\t', it, code_type) for it in item_list)
if os.path.exists(path): if os.path.exists(path):
with open(path, 'r', encoding='utf-8-sig') as f: # newline='' => без трансляции переводов строк: иначе CRLF молча схлопнется
# в LF при чтении и файл будет переписан в LF (#44/#46/#47).
with open(path, 'r', encoding='utf-8-sig', newline='') as f:
text = f.read() text = f.read()
text = text.replace('</PredefinedData>', items_xml + '</PredefinedData>') text = text.replace('</PredefinedData>', items_xml + '</PredefinedData>')
else: else:
@@ -3103,7 +3200,10 @@ def add_predefined_items(items):
'xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" ' 'xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' 'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
f'xsi:type="{xsi_type}" version="{version}">\r\n') f'xsi:type="{xsi_type}" version="{version}">\r\n')
text = hdr + items_xml + '</PredefinedData>\r\n' text = hdr + items_xml + '</PredefinedData>'
# Без перевода строки в конце — канон #57. Срезаем в ОБЕИХ ветках: файл, созданный
# прежней версией навыка, мог унести хвост, а PS-порт срезает безусловно.
text = text.rstrip('\r\n')
with open(path, 'wb') as f: with open(path, 'wb') as f:
f.write(b'\xef\xbb\xbf') f.write(b'\xef\xbb\xbf')
f.write(text.encode('utf-8')) f.write(text.encode('utf-8'))
@@ -3141,7 +3241,7 @@ def main():
parser.add_argument("-Operation", default=None, choices=valid_operations, help="Inline operation") parser.add_argument("-Operation", default=None, choices=valid_operations, help="Inline operation")
parser.add_argument("-Value", default=None, help="Inline value") parser.add_argument("-Value", default=None, help="Inline value")
parser.add_argument("-NoValidate", action="store_true", help="Skip auto-validation") parser.add_argument("-NoValidate", action="store_true", help="Skip auto-validation")
args = parser.parse_args() args = ci_parse_args(parser)
# --- Mode validation --- # --- Mode validation ---
if args.DefinitionFile and args.Operation: if args.DefinitionFile and args.Operation:
@@ -3155,7 +3255,7 @@ def main():
if not os.path.exists(args.DefinitionFile): if not os.path.exists(args.DefinitionFile):
die(f"Definition file not found: {args.DefinitionFile}") die(f"Definition file not found: {args.DefinitionFile}")
with open(args.DefinitionFile, "r", encoding="utf-8-sig") as f: with open(args.DefinitionFile, "r", encoding="utf-8-sig") as f:
definition = json.load(f) definition = ci_json(json.load(f))
# --- Resolve object path --- # --- Resolve object path ---
object_path = args.ObjectPath object_path = args.ObjectPath
@@ -3192,6 +3292,13 @@ def main():
xml_tree = etree.parse(resolved_path, xml_parser) xml_tree = etree.parse(resolved_path, xml_parser)
xml_root = xml_tree.getroot() xml_root = xml_tree.getroot()
# Префикс current-config берём из объявлений корня — им и пишем ссылочные типы.
global cfg_prefix, is_format_221
cfg_prefix = next((p for p, u in (xml_root.nsmap or {}).items() if u == CFG_NS and p), None)
_fv = xml_root.get("version") or "2.17"
_m = re.match(r'^(\d+)\.(\d+)$', _fv)
is_format_221 = bool(_m) and int(_m.group(1)) * 100 + int(_m.group(2)) >= 221
# --- Detect object type --- # --- Detect object type ---
if localname(xml_root) != "MetaDataObject": if localname(xml_root) != "MetaDataObject":
die(f"Root element must be MetaDataObject, got: {localname(xml_root)}") die(f"Root element must be MetaDataObject, got: {localname(xml_root)}")
+3 -1
View File
@@ -34,7 +34,9 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-info.ps1" -Obj
| `brief` | Всё одной-двумя строками: имена полей, счётчики | | `brief` | Всё одной-двумя строками: имена полей, счётчики |
| `full` | Всё раскрыто: колонки ТЧ, список источников подписки, движения, формы | | `full` | Всё раскрыто: колонки ТЧ, список источников подписки, движения, формы |
`-Name` — drill-down: раскрыть конкретный элемент объекта (ТЧ, реквизит, шаблон URL, операцию веб-сервиса). `-Name` — drill-down: раскрыть конкретный элемент объекта (ТЧ, реквизит, стандартный реквизит,
шаблон URL, операцию веб-сервиса). Составной тип в сводке свёрнут в счётчик — полный список
типов даёт drill-down: `-Name ТипЗначения` у ПВХ, `-Name Владелец` у подчинённого справочника.
## Поддерживаемые типы (23) ## Поддерживаемые типы (23)
+316 -44
View File
@@ -1,4 +1,4 @@
# meta-info v1.4 — Compact summary of 1C metadata object # meta-info v1.10 — Compact summary of 1C metadata object (+единое имя хелпера состояния поддержки)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath, [Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath,
@@ -170,6 +170,22 @@ function Get-MLText($node) {
return "" return ""
} }
# Тип-множество: голое имя метатипа без `.Имя` означает ВСЕ ссылки этого класса
# (см. docs/meta-dsl-spec.md §«Тип-множество»). Конкретный тип всегда пишется с точкой,
# поэтому «СправочникСсылка» без точки читается однозначно как обобщённый.
function Format-SingleTypeSet([string]$raw) {
$raw = $raw -replace '^d\d+p\d+:', 'cfg:'
if ($raw -match '^cfg:DefinedType\.(.+)$') { return "ОпределяемыйТип.$($Matches[1])" }
if ($raw -match '^cfg:Characteristic\.(.+)$') { return "Характеристика.$($Matches[1])" }
if ($raw -eq 'cfg:AnyRef') { return "ЛюбаяСсылка" }
if ($raw -eq 'cfg:AnyIBRef') { return "ЛюбаяСсылкаИБ" }
if ($raw -match '^cfg:(\w+Ref)$' -and $refTypeMap.ContainsKey($Matches[1])) {
return $refTypeMap[$Matches[1]]
}
if ($raw -match '^cfg:(.+)$') { return $Matches[1] }
return $raw
}
function Format-Type($typeNode) { function Format-Type($typeNode) {
if (-not $typeNode) { return "" } if (-not $typeNode) { return "" }
$types = @() $types = @()
@@ -178,14 +194,7 @@ function Format-Type($typeNode) {
$types += Format-SingleType $raw $typeNode $types += Format-SingleType $raw $typeNode
} }
foreach ($t in $typeNode.SelectNodes("v8:TypeSet", $ns)) { foreach ($t in $typeNode.SelectNodes("v8:TypeSet", $ns)) {
$raw = $t.InnerText $types += Format-SingleTypeSet $t.InnerText
if ($raw -match '^cfg:DefinedType\.(.+)$') {
$types += "ОпределяемыйТип.$($Matches[1])"
} elseif ($raw -match '^cfg:Characteristic\.(.+)$') {
$types += "Характеристика.$($Matches[1])"
} else {
$types += $raw
}
} }
if ($types.Count -eq 0) { return "" } if ($types.Count -eq 0) { return "" }
if ($types.Count -eq 1) { return $types[0] } if ($types.Count -eq 1) { return $types[0] }
@@ -287,6 +296,202 @@ function Format-Flags($propsNode, [bool]$isDimension = $false) {
return " [$($flags -join ', ')]" return " [$($flags -join ', ')]"
} }
# Ссылка на объект метаданных (`Catalog.Валюты` в Owners и подобных) — не тип значения,
# но в выводе показываем именно тип, который присваивается: СправочникСсылка.Валюты.
function Format-MDObjectRef([string]$raw) {
if ($raw -match '^(\w+)\.(.+)$') {
$key = "$($Matches[1])Ref"
if ($refTypeMap.ContainsKey($key)) { return "$($refTypeMap[$key]).$($Matches[2])" }
}
return $raw
}
# --- Стандартные реквизиты ---
# Сколько типов состава печатать списком, прежде чем свернуть в счётчик. По корпусу
# acc/erp/ut/unf состав ПВХ доходит до 151 типа, при этом 20 из 42 ПВХ укладываются в 5.
$script:composedTypeThreshold = 5
# Имена полей, состав которых свернули в счётчик, — по ним в конце вывода печатается
# единственная подсказка, чем состав развернуть.
$script:collapsedNames = @()
# Блок StandardAttributes в XML опционален: платформа материализует его только когда хотя бы
# один стандартный реквизит кастомизирован (docs/meta-dsl-spec.md §7.1.1). Когда блока нет,
# действуют платформенные дефолты — профиль ниже совпадает с $stdAttrProfile в meta-compile
# (выведен из корпуса acc+erp). Правки держать синхронными, иначе навыки разъедутся молча.
$stdAttrRequiredDefault = @{
"Catalog" = @{ "Owner" = $true; "Description" = $true }
"Document" = @{ "Date" = $true }
"ExchangePlan" = @{ "Description" = $true; "Code" = $true }
"ChartOfAccounts" = @{ "Description" = $true; "Code" = $true }
"ChartOfCharacteristicTypes" = @{ "Description" = $true }
"ChartOfCalculationTypes" = @{ "Description" = $true }
}
function Test-StdAttrRequired($propsNode, [string]$attrName, [string]$objType) {
$fc = $propsNode.SelectSingleNode("md:StandardAttributes/xr:StandardAttribute[@name='$attrName']/xr:FillChecking", $ns)
if ($fc) { return ($fc.InnerText -eq "ShowError") }
if ($stdAttrRequiredDefault.ContainsKey($objType) -and $stdAttrRequiredDefault[$objType].ContainsKey($attrName)) {
return $stdAttrRequiredDefault[$objType][$attrName]
}
return $false
}
function New-StdAttr([string]$name, [string]$type, $flags) {
$f = @($flags | Where-Object { $_ })
$flagStr = if ($f.Count -gt 0) { " [$($f -join ', ')]" } else { "" }
return @{ Name = $name; Type = $type; Flags = $flagStr }
}
# Стандартные реквизиты, наличие и характеристики которых задаются настройками объекта, —
# их нельзя вывести из остального вывода, поэтому они попадают и в overview. Ссылка,
# ПометкаУдаления, Родитель и прочее следуют из типа объекта и строки «Иерархический» —
# они только в full (см. Get-StandardAttributesFull).
function Get-StandardAttributes($propsNode, [string]$objType, [string]$mode) {
$result = @()
# Владелец — только у подчинённого справочника; состав типов ниоткуда не выводится
if ($objType -eq "Catalog") {
$ownersNode = $propsNode.SelectSingleNode("md:Owners", $ns)
$ownerTypes = @()
if ($ownersNode) {
foreach ($it in $ownersNode.SelectNodes("xr:Item", $ns)) {
$ownerTypes += Format-MDObjectRef $it.InnerText
}
}
if ($ownerTypes.Count -gt 0) {
$req = if (Test-StdAttrRequired $propsNode "Owner" $objType) { "обязательный" } else { $null }
$result += New-StdAttr "Владелец" ($ownerTypes -join ", ") @($req)
}
}
# ТипЗначения ПВХ — до полутора сотен типов, полный список раздул бы сводку в обоих
# режимах, поэтому сворачиваем в счётчик. Состав смотреть через -Name ТипЗначения.
if ($objType -eq "ChartOfCharacteristicTypes") {
$vt = $propsNode.SelectSingleNode("md:Type", $ns)
if ($vt) {
$cnt = $vt.SelectNodes("v8:Type", $ns).Count + $vt.SelectNodes("v8:TypeSet", $ns).Count
$typeStr = if ($cnt -gt $script:composedTypeThreshold) {
$script:collapsedNames += "ТипЗначения"
"Составной ($cnt)"
} else { Format-Type $vt }
if ($typeStr) { $result += New-StdAttr "ТипЗначения" $typeStr @() }
}
}
# Дата — есть всегда, ценна флагом обязательности
if ($objType -in @("Document", "BusinessProcess", "Task")) {
$req = if (Test-StdAttrRequired $propsNode "Date" $objType) { "обязательный" } else { $null }
$result += New-StdAttr "Дата" "Дата" @($req)
}
# Номер — тип и длина задаются объектом, при NumberLength=0 номера нет
if ($objType -in @("Document", "BusinessProcess", "Task")) {
$numLen = $propsNode.SelectSingleNode("md:NumberLength", $ns)
if ($numLen -and [int]$numLen.InnerText -gt 0) {
$numType = $propsNode.SelectSingleNode("md:NumberType", $ns)
$ntRu = if ($numType -and $numType.InnerText -eq "Number") { "Число" } else { "Строка" }
$flags = @()
if (Test-StdAttrRequired $propsNode "Number" $objType) { $flags += "обязательный" }
$numPer = $propsNode.SelectSingleNode("md:NumberPeriodicity", $ns)
if ($numPer -and $numberPeriodMap.ContainsKey($numPer.InnerText)) { $flags += $numberPeriodMap[$numPer.InnerText] }
$numAllowed = $propsNode.SelectSingleNode("md:NumberAllowedLength", $ns)
if ($numAllowed -and $numAllowed.InnerText -eq "Fixed") { $flags += "фикс. длина" }
$autoNum = $propsNode.SelectSingleNode("md:Autonumbering", $ns)
if ($autoNum -and $autoNum.InnerText -eq "true") { $flags += "авто" }
$result += New-StdAttr "Номер" "$ntRu($($numLen.InnerText))" $flags
}
}
# Код — при CodeLength=0 кода у объекта нет вовсе, строку не печатаем
$codeLen = $propsNode.SelectSingleNode("md:CodeLength", $ns)
if ($codeLen -and [int]$codeLen.InnerText -gt 0) {
$codeType = $propsNode.SelectSingleNode("md:CodeType", $ns)
$ctRu = if ($codeType -and $codeType.InnerText -eq "Number") { "Число" } else { "Строка" }
$flags = @()
if (Test-StdAttrRequired $propsNode "Code" $objType) { $flags += "обязательный" }
$codeAllowed = $propsNode.SelectSingleNode("md:CodeAllowedLength", $ns)
if ($codeAllowed -and $codeAllowed.InnerText -eq "Fixed") { $flags += "фикс. длина" }
$result += New-StdAttr "Код" "$ctRu($($codeLen.InnerText))" $flags
}
# Наименование — при DescriptionLength=0 наименования нет
$descLen = $propsNode.SelectSingleNode("md:DescriptionLength", $ns)
if ($descLen -and [int]$descLen.InnerText -gt 0) {
$req = if (Test-StdAttrRequired $propsNode "Description" $objType) { "обязательный" } else { $null }
$result += New-StdAttr "Наименование" "Строка($($descLen.InnerText))" @($req)
}
return $result
}
# Остальные стандартные реквизиты — предсказуемы по типу объекта, но в full полезны
# как перечень доступных полей для запроса.
function Get-StandardAttributesFull($propsNode, [string]$objType, [string]$objName) {
$selfRef = switch ($objType) {
"Catalog" { "СправочникСсылка.$objName" }
"ChartOfCharacteristicTypes" { "ПВХСсылка.$objName" }
"ChartOfAccounts" { "ПланСчетовСсылка.$objName" }
"ChartOfCalculationTypes" { "ПВРСсылка.$objName" }
"ExchangePlan" { "ПланОбменаСсылка.$objName" }
"Document" { "ДокументСсылка.$objName" }
"BusinessProcess" { "БизнесПроцессСсылка.$objName" }
"Task" { "ЗадачаСсылка.$objName" }
default { "" }
}
$result = @()
if ($selfRef) { $result += New-StdAttr "Ссылка" $selfRef @() }
$result += New-StdAttr "ПометкаУдаления" "Булево" @()
# Родитель и ЭтоГруппа существуют только у иерархических объектов
$hier = $propsNode.SelectSingleNode("md:Hierarchical", $ns)
$isHier = ($hier -and $hier.InnerText -eq "true") -or $objType -eq "ChartOfAccounts"
if ($isHier -and $selfRef) {
$result += New-StdAttr "Родитель" $selfRef @()
$ht = $propsNode.SelectSingleNode("md:HierarchyType", $ns)
if ($objType -eq "Catalog" -and (-not $ht -or $ht.InnerText -eq "HierarchyFoldersAndItems")) {
$result += New-StdAttr "ЭтоГруппа" "Булево" @()
}
}
switch ($objType) {
"Document" {
$result += New-StdAttr "Проведен" "Булево" @()
}
"ExchangePlan" {
$result += New-StdAttr "ЭтотУзел" "Булево" @()
$result += New-StdAttr "НомерОтправленного" "Число" @()
$result += New-StdAttr "НомерПринятого" "Число" @()
}
"BusinessProcess" {
$result += New-StdAttr "Стартован" "Булево" @()
$result += New-StdAttr "Завершен" "Булево" @()
$result += New-StdAttr "ВедущаяЗадача" "ЗадачаСсылка" @()
}
"Task" {
$result += New-StdAttr "Выполнена" "Булево" @()
$result += New-StdAttr "БизнесПроцесс" "БизнесПроцессСсылка" @()
$result += New-StdAttr "ТочкаМаршрута" "БизнесПроцессТочкаМаршрутаСсылка" @()
}
"ChartOfAccounts" {
$result += New-StdAttr "Вид" "ВидСчета" @()
$result += New-StdAttr "Забалансовый" "Булево" @()
$result += New-StdAttr "Порядок" "Число" @()
}
"ChartOfCalculationTypes" {
$result += New-StdAttr "ПериодДействияБазовый" "Булево" @()
}
}
# Предопределённые данные есть у всех перечисленных типов, кроме документов и задач
if ($objType -in @("Catalog", "ChartOfCharacteristicTypes", "ChartOfAccounts", "ChartOfCalculationTypes")) {
$result += New-StdAttr "Предопределенный" "Булево" @()
$result += New-StdAttr "ИмяПредопределенныхДанных" "Строка" @()
}
return $result
}
function Get-Attributes($parentNode, [string]$childTag = "Attribute", [bool]$isDimension = $false) { function Get-Attributes($parentNode, [string]$childTag = "Attribute", [bool]$isDimension = $false) {
$result = @() $result = @()
foreach ($attr in $parentNode.SelectNodes("md:$childTag", $ns)) { foreach ($attr in $parentNode.SelectNodes("md:$childTag", $ns)) {
@@ -329,7 +534,10 @@ function Get-MaxNameLen($attrs) {
function Get-SimpleChildren($parentNode, [string]$tag) { function Get-SimpleChildren($parentNode, [string]$tag) {
$result = @() $result = @()
foreach ($child in $parentNode.SelectNodes("md:$tag", $ns)) { foreach ($child in $parentNode.SelectNodes("md:$tag", $ns)) {
$result += $child.InnerText # Form/Template в ChildObjects — простые узлы с именем в тексте, а Command — узел
# с вложенным Properties: у него InnerText склеил бы всё содержимое в одну строку.
$nameNode = $child.SelectSingleNode("md:Properties/md:Name", $ns)
if ($nameNode) { $result += $nameNode.InnerText } else { $result += $child.InnerText }
} }
return $result return $result
} }
@@ -650,6 +858,43 @@ if ($Name -and $childObjs) {
} }
} }
# Не нашли среди дочерних объектов — пробуем стандартные реквизиты ниже
}
# Drill-down по стандартному реквизиту — единственный способ увидеть состав типов целиком,
# когда в сводке он свёрнут в счётчик. Живёт в Properties, а не в ChildObjects, поэтому идёт
# отдельной веткой и работает даже у объектов без ChildObjects.
if ($Name -and -not $drillDone) {
$stdAll = @(Get-StandardAttributes $props $mdType "full") + @(Get-StandardAttributesFull $props $mdType $objName)
foreach ($s in $stdAll) {
if ($s.Name -ne $Name) { continue }
Out "Стандартный реквизит: $($s.Name)"
# Состав типов разворачиваем списком: ради этого drill-down и нужен
$typeSrc = $null
if ($Name -eq "ТипЗначения") { $typeSrc = $props.SelectSingleNode("md:Type", $ns) }
$typeList = @()
if ($typeSrc) {
foreach ($t in $typeSrc.SelectNodes("v8:Type", $ns)) { $typeList += Format-SingleType $t.InnerText $typeSrc }
foreach ($t in $typeSrc.SelectNodes("v8:TypeSet", $ns)) { $typeList += Format-SingleTypeSet $t.InnerText }
} elseif ($Name -eq "Владелец") {
$ownersNode = $props.SelectSingleNode("md:Owners", $ns)
if ($ownersNode) {
foreach ($it in $ownersNode.SelectNodes("xr:Item", $ns)) { $typeList += Format-MDObjectRef $it.InnerText }
}
}
if ($typeList.Count -gt 0) {
Out " Типы ($($typeList.Count)):"
foreach ($t in $typeList) { Out " $t" }
} else {
Out " Тип: $($s.Type)"
}
$flagText = $s.Flags.Trim()
if ($flagText) { Out " Свойства: $($flagText.Trim('[', ']'))" }
$drillDone = $true
break
}
if (-not $drillDone) { if (-not $drillDone) {
Write-Host "[ERROR] '$Name' not found in $objName" Write-Host "[ERROR] '$Name' not found in $objName"
exit 1 exit 1
@@ -680,6 +925,20 @@ if (-not $drillDone) {
# --- Mode: brief --- # --- Mode: brief ---
if ($Mode -eq "brief") { if ($Mode -eq "brief") {
# Подчинённость — единственный факт структуры, который из brief не выводится никак,
# а без него код записи элемента падает. Про обязательность здесь намеренно молчим:
# обязательными бывают и обычные реквизиты, а их brief не разбирает.
if ($mdType -eq "Catalog") {
$ownersNode = $props.SelectSingleNode("md:Owners", $ns)
$ownerNames = @()
if ($ownersNode) {
foreach ($it in $ownersNode.SelectNodes("xr:Item", $ns)) {
$ownerNames += ($it.InnerText -replace '^\w+\.', '')
}
}
if ($ownerNames.Count -gt 0) { Out "Подчинён: $($ownerNames -join ', ')" }
}
# Attributes # Attributes
$attrs = @() $attrs = @()
if ($childObjs) { $attrs = @(Get-Attributes $childObjs) } if ($childObjs) { $attrs = @(Get-Attributes $childObjs) }
@@ -828,51 +1087,47 @@ if (-not $drillDone) {
# Document-specific header properties # Document-specific header properties
if ($mdType -eq "Document") { if ($mdType -eq "Document") {
$numType = $props.SelectSingleNode("md:NumberType", $ns)
$numLen = $props.SelectSingleNode("md:NumberLength", $ns)
$numPer = $props.SelectSingleNode("md:NumberPeriodicity", $ns)
$autoNum = $props.SelectSingleNode("md:Autonumbering", $ns)
$posting = $props.SelectSingleNode("md:Posting", $ns) $posting = $props.SelectSingleNode("md:Posting", $ns)
$parts = @() $parts = @()
if ($numType -and $numLen) { # Номер уехал в блок стандартных реквизитов — здесь остаются свойства объекта
$nt = if ($numType.InnerText -eq "String") { "Строка" } else { "Число" }
$piece = "Номер: $nt($($numLen.InnerText))"
if ($numPer) {
$perRu = if ($numberPeriodMap.ContainsKey($numPer.InnerText)) { $numberPeriodMap[$numPer.InnerText] } else { $numPer.InnerText }
$piece += ", $perRu"
}
if ($autoNum -and $autoNum.InnerText -eq "true") { $piece += ", авто" }
$parts += $piece
}
if ($posting) { if ($posting) {
$parts += "Проведение: $(if ($posting.InnerText -eq 'Allow') { 'да' } else { 'нет' })" $parts += "Проведение: $(if ($posting.InnerText -eq 'Allow') { 'да' } else { 'нет' })"
} }
if ($parts.Count -gt 0) { Out ($parts -join " | ") } if ($parts.Count -gt 0) { Out ($parts -join " | ") }
} }
# Catalog-specific header properties # Свойства иерархии и подчинения. Иерархия — не только у справочников: свойство
if ($mdType -eq "Catalog") { # Hierarchical есть и у ПВХ (в корпусе acc/erp/ut/unf их 13), а без этой строки
$parts = @() # в full появлялся Родитель, происхождение которого было ниоткуда не видно.
$hier = $props.SelectSingleNode("md:Hierarchical", $ns) $parts = @()
if ($hier -and $hier.InnerText -eq "true") { $hier = $props.SelectSingleNode("md:Hierarchical", $ns)
$ht = $props.SelectSingleNode("md:HierarchyType", $ns) if ($hier -and $hier.InnerText -eq "true") {
$htText = if ($ht -and $ht.InnerText -eq "HierarchyFoldersAndItems") { "группы и элементы" } else { "элементы" } $ht = $props.SelectSingleNode("md:HierarchyType", $ns)
$limitNode = $props.SelectSingleNode("md:LimitLevelCount", $ns) $htText = if ($ht -and $ht.InnerText -eq "HierarchyFoldersAndItems") { "группы и элементы" } else { "элементы" }
$levelNode = $props.SelectSingleNode("md:LevelCount", $ns) $limitNode = $props.SelectSingleNode("md:LimitLevelCount", $ns)
if ($limitNode -and $limitNode.InnerText -eq "true" -and $levelNode) { $levelNode = $props.SelectSingleNode("md:LevelCount", $ns)
$htText += ", уровней: $($levelNode.InnerText)" if ($limitNode -and $limitNode.InnerText -eq "true" -and $levelNode) {
} else { $htText += ", уровней: $($levelNode.InnerText)"
$htText += ", без ограничения уровней" } else {
} $htText += ", без ограничения уровней"
$parts += "Иерархический: $htText"
} }
$codeLen = $props.SelectSingleNode("md:CodeLength", $ns) $parts += "Иерархический: $htText"
$descLen = $props.SelectSingleNode("md:DescriptionLength", $ns)
if ($codeLen -and [int]$codeLen.InnerText -gt 0) { $parts += "Код($($codeLen.InnerText))" }
if ($descLen -and [int]$descLen.InnerText -gt 0) { $parts += "Наименование($($descLen.InnerText))" }
if ($parts.Count -gt 0) { Out ($parts -join " | ") }
} }
# Код и Наименование уехали в блок стандартных реквизитов — здесь только свойства объекта.
# Подчинение печатаем лишь когда оно отличается от дефолта ToItems.
if ($mdType -eq "Catalog") {
$sub = $props.SelectSingleNode("md:SubordinationUse", $ns)
if ($sub -and $sub.InnerText -ne "ToItems") {
$subRu = switch ($sub.InnerText) {
"ToFolders" { "группам" }
"ToFoldersAndItems" { "группам и элементам" }
default { $sub.InnerText }
}
$parts += "Подчинение: $subRu"
}
}
if ($parts.Count -gt 0) { Out ($parts -join " | ") }
# Register-specific header properties # Register-specific header properties
if ($mdType -match "Register$") { if ($mdType -match "Register$") {
@@ -1059,6 +1314,16 @@ if (-not $drillDone) {
} }
} }
# --- Standard attributes ---
$stdAttrs = @(Get-StandardAttributes $props $mdType $Mode)
if ($Mode -eq "full") { $stdAttrs += @(Get-StandardAttributesFull $props $mdType $objName) }
if ($stdAttrs.Count -gt 0) {
Out ""
Out "Стандартные реквизиты:"
$ml = Get-MaxNameLen $stdAttrs
foreach ($s in $stdAttrs) { Out (Format-AttrLine $s $ml) }
}
# --- Dimensions (registers) --- # --- Dimensions (registers) ---
if ($mdType -match "Register$" -and $childObjs) { if ($mdType -match "Register$" -and $childObjs) {
$dims = @(Get-Attributes $childObjs "Dimension" $true) $dims = @(Get-Attributes $childObjs "Dimension" $true)
@@ -1177,6 +1442,13 @@ if (-not $drillDone) {
} }
} }
} }
# Единственная подсказка на весь вывод — чем развернуть свёрнутый в счётчик состав
if ($script:collapsedNames.Count -gt 0) {
$hints = ($script:collapsedNames | ForEach-Object { "-Name $_" }) -join ", "
Out ""
Out "Полный состав типов: $hints"
}
} }
# --- Pagination and output --- # --- Pagination and output ---
+331 -49
View File
@@ -1,4 +1,4 @@
# meta-info v1.4 — Compact summary of 1C metadata object (Python port) # meta-info v1.10 — Compact summary of 1C metadata object (Python port) (+единое имя хелпера состояния поддержки)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import os import os
@@ -10,6 +10,28 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ── arg parsing ────────────────────────────────────────────── # ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False) parser = argparse.ArgumentParser(allow_abbrev=False)
@@ -19,7 +41,7 @@ parser.add_argument("-Name", default="")
parser.add_argument("-Limit", type=int, default=150) parser.add_argument("-Limit", type=int, default=150)
parser.add_argument("-Offset", type=int, default=0) parser.add_argument("-Offset", type=int, default=0)
parser.add_argument("-OutFile", default="") parser.add_argument("-OutFile", default="")
args = parser.parse_args() args = ci_parse_args(parser)
object_path = args.ObjectPath object_path = args.ObjectPath
mode = args.Mode mode = args.Mode
@@ -227,6 +249,30 @@ def get_ml_text(node):
return "" return ""
# Тип-множество: голое имя метатипа без `.Имя` означает ВСЕ ссылки этого класса
# (см. docs/meta-dsl-spec.md §«Тип-множество»). Конкретный тип всегда пишется с точкой,
# поэтому «СправочникСсылка» без точки читается однозначно как обобщённый.
def format_single_type_set(raw):
raw = re.sub(r'^d\d+p\d+:', 'cfg:', raw)
m = re.match(r'^cfg:DefinedType\.(.+)$', raw)
if m:
return f"ОпределяемыйТип.{m.group(1)}"
m = re.match(r'^cfg:Characteristic\.(.+)$', raw)
if m:
return f"Характеристика.{m.group(1)}"
if raw == "cfg:AnyRef":
return "ЛюбаяСсылка"
if raw == "cfg:AnyIBRef":
return "ЛюбаяСсылкаИБ"
m = re.match(r'^cfg:(\w+Ref)$', raw)
if m and m.group(1) in ref_type_map:
return ref_type_map[m.group(1)]
m = re.match(r'^cfg:(.+)$', raw)
if m:
return m.group(1)
return raw
def format_type(type_node_el): def format_type(type_node_el):
if type_node_el is None: if type_node_el is None:
return "" return ""
@@ -234,16 +280,7 @@ def format_type(type_node_el):
for t in find_all(type_node_el, "v8:Type"): for t in find_all(type_node_el, "v8:Type"):
types.append(format_single_type(inner_text(t), type_node_el)) types.append(format_single_type(inner_text(t), type_node_el))
for t in find_all(type_node_el, "v8:TypeSet"): for t in find_all(type_node_el, "v8:TypeSet"):
raw = inner_text(t) types.append(format_single_type_set(inner_text(t)))
m = re.match(r'^cfg:DefinedType\.(.+)$', raw)
if m:
types.append(f"ОпределяемыйТип.{m.group(1)}")
continue
m = re.match(r'^cfg:Characteristic\.(.+)$', raw)
if m:
types.append(f"Характеристика.{m.group(1)}")
continue
types.append(raw)
if len(types) == 0: if len(types) == 0:
return "" return ""
if len(types) == 1: if len(types) == 1:
@@ -341,6 +378,190 @@ def format_flags(a_props, is_dimension=False):
return f" [{', '.join(flags)}]" return f" [{', '.join(flags)}]"
# Ссылка на объект метаданных (`Catalog.Валюты` в Owners и подобных) — не тип значения,
# но в выводе показываем именно тип, который присваивается: СправочникСсылка.Валюты.
def format_md_object_ref(raw):
m = re.match(r'^(\w+)\.(.+)$', raw)
if m:
key = f"{m.group(1)}Ref"
if key in ref_type_map:
return f"{ref_type_map[key]}.{m.group(2)}"
return raw
# ── Стандартные реквизиты ────────────────────────────────────
# Сколько типов состава печатать списком, прежде чем свернуть в счётчик. По корпусу
# acc/erp/ut/unf состав ПВХ доходит до 151 типа, при этом 20 из 42 ПВХ укладываются в 5.
COMPOSED_TYPE_THRESHOLD = 5
# Имена полей, состав которых свернули в счётчик, — по ним в конце вывода печатается
# единственная подсказка, чем состав развернуть.
collapsed_names = []
# Блок StandardAttributes в XML опционален: платформа материализует его только когда хотя бы
# один стандартный реквизит кастомизирован (docs/meta-dsl-spec.md §7.1.1). Когда блока нет,
# действуют платформенные дефолты — профиль ниже совпадает с $stdAttrProfile в meta-compile
# (выведен из корпуса acc+erp). Правки держать синхронными, иначе навыки разъедутся молча.
std_attr_required_default = {
"Catalog": {"Owner": True, "Description": True},
"Document": {"Date": True},
"ExchangePlan": {"Description": True, "Code": True},
"ChartOfAccounts": {"Description": True, "Code": True},
"ChartOfCharacteristicTypes": {"Description": True},
"ChartOfCalculationTypes": {"Description": True},
}
def test_std_attr_required(props_node, attr_name, obj_type):
fc = find(props_node, f"md:StandardAttributes/xr:StandardAttribute[@name='{attr_name}']/xr:FillChecking")
if fc is not None:
return inner_text(fc) == "ShowError"
return std_attr_required_default.get(obj_type, {}).get(attr_name, False)
def new_std_attr(name, type_str, flags):
f = [x for x in flags if x]
flag_str = f" [{', '.join(f)}]" if f else ""
return {"Name": name, "Type": type_str, "Flags": flag_str}
# Стандартные реквизиты, наличие и характеристики которых задаются настройками объекта, —
# их нельзя вывести из остального вывода, поэтому они попадают и в overview. Ссылка,
# ПометкаУдаления, Родитель и прочее следуют из типа объекта и строки «Иерархический» —
# они только в full (см. get_standard_attributes_full).
def get_standard_attributes(props_node, obj_type, mode_name):
result = []
# Владелец — только у подчинённого справочника; состав типов ниоткуда не выводится
if obj_type == "Catalog":
owners_node = find(props_node, "md:Owners")
owner_types = []
if owners_node is not None:
for it in find_all(owners_node, "xr:Item"):
owner_types.append(format_md_object_ref(inner_text(it)))
if owner_types:
req = "обязательный" if test_std_attr_required(props_node, "Owner", obj_type) else None
result.append(new_std_attr("Владелец", ", ".join(owner_types), [req]))
# ТипЗначения ПВХ — до полутора сотен типов, полный список раздул бы сводку в обоих
# режимах, поэтому сворачиваем в счётчик. Состав смотреть через -Name ТипЗначения.
if obj_type == "ChartOfCharacteristicTypes":
vt = find(props_node, "md:Type")
if vt is not None:
cnt = len(find_all(vt, "v8:Type")) + len(find_all(vt, "v8:TypeSet"))
if cnt > COMPOSED_TYPE_THRESHOLD:
type_str = f"Составной ({cnt})"
collapsed_names.append("ТипЗначения")
else:
type_str = format_type(vt)
if type_str:
result.append(new_std_attr("ТипЗначения", type_str, []))
# Дата — есть всегда, ценна флагом обязательности
if obj_type in ("Document", "BusinessProcess", "Task"):
req = "обязательный" if test_std_attr_required(props_node, "Date", obj_type) else None
result.append(new_std_attr("Дата", "Дата", [req]))
# Номер — тип и длина задаются объектом, при NumberLength=0 номера нет
if obj_type in ("Document", "BusinessProcess", "Task"):
num_len = find(props_node, "md:NumberLength")
if num_len is not None and inner_text(num_len).isdigit() and int(inner_text(num_len)) > 0:
num_type = find(props_node, "md:NumberType")
nt_ru = "Число" if num_type is not None and inner_text(num_type) == "Number" else "Строка"
flags = []
if test_std_attr_required(props_node, "Number", obj_type):
flags.append("обязательный")
num_per = find(props_node, "md:NumberPeriodicity")
if num_per is not None and inner_text(num_per) in number_period_map:
flags.append(number_period_map[inner_text(num_per)])
num_allowed = find(props_node, "md:NumberAllowedLength")
if num_allowed is not None and inner_text(num_allowed) == "Fixed":
flags.append("фикс. длина")
auto_num = find(props_node, "md:Autonumbering")
if auto_num is not None and inner_text(auto_num) == "true":
flags.append("авто")
result.append(new_std_attr("Номер", f"{nt_ru}({inner_text(num_len)})", flags))
# Код — при CodeLength=0 кода у объекта нет вовсе, строку не печатаем
code_len = find(props_node, "md:CodeLength")
if code_len is not None and inner_text(code_len).isdigit() and int(inner_text(code_len)) > 0:
code_type = find(props_node, "md:CodeType")
ct_ru = "Число" if code_type is not None and inner_text(code_type) == "Number" else "Строка"
flags = []
if test_std_attr_required(props_node, "Code", obj_type):
flags.append("обязательный")
code_allowed = find(props_node, "md:CodeAllowedLength")
if code_allowed is not None and inner_text(code_allowed) == "Fixed":
flags.append("фикс. длина")
result.append(new_std_attr("Код", f"{ct_ru}({inner_text(code_len)})", flags))
# Наименование — при DescriptionLength=0 наименования нет
desc_len = find(props_node, "md:DescriptionLength")
if desc_len is not None and inner_text(desc_len).isdigit() and int(inner_text(desc_len)) > 0:
req = "обязательный" if test_std_attr_required(props_node, "Description", obj_type) else None
result.append(new_std_attr("Наименование", f"Строка({inner_text(desc_len)})", [req]))
return result
# Остальные стандартные реквизиты — предсказуемы по типу объекта, но в full полезны
# как перечень доступных полей для запроса.
def get_standard_attributes_full(props_node, obj_type, obj_name):
self_ref_map = {
"Catalog": "СправочникСсылка",
"ChartOfCharacteristicTypes": "ПВХСсылка",
"ChartOfAccounts": "ПланСчетовСсылка",
"ChartOfCalculationTypes": "ПВРСсылка",
"ExchangePlan": "ПланОбменаСсылка",
"Document": "ДокументСсылка",
"BusinessProcess": "БизнесПроцессСсылка",
"Task": "ЗадачаСсылка",
}
self_ref = f"{self_ref_map[obj_type]}.{obj_name}" if obj_type in self_ref_map else ""
result = []
if self_ref:
result.append(new_std_attr("Ссылка", self_ref, []))
result.append(new_std_attr("ПометкаУдаления", "Булево", []))
# Родитель и ЭтоГруппа существуют только у иерархических объектов
hier = find(props_node, "md:Hierarchical")
is_hier = (hier is not None and inner_text(hier) == "true") or obj_type == "ChartOfAccounts"
if is_hier and self_ref:
result.append(new_std_attr("Родитель", self_ref, []))
ht = find(props_node, "md:HierarchyType")
if obj_type == "Catalog" and (ht is None or inner_text(ht) == "HierarchyFoldersAndItems"):
result.append(new_std_attr("ЭтоГруппа", "Булево", []))
if obj_type == "Document":
result.append(new_std_attr("Проведен", "Булево", []))
elif obj_type == "ExchangePlan":
result.append(new_std_attr("ЭтотУзел", "Булево", []))
result.append(new_std_attr("НомерОтправленного", "Число", []))
result.append(new_std_attr("НомерПринятого", "Число", []))
elif obj_type == "BusinessProcess":
result.append(new_std_attr("Стартован", "Булево", []))
result.append(new_std_attr("Завершен", "Булево", []))
result.append(new_std_attr("ВедущаяЗадача", "ЗадачаСсылка", []))
elif obj_type == "Task":
result.append(new_std_attr("Выполнена", "Булево", []))
result.append(new_std_attr("БизнесПроцесс", "БизнесПроцессСсылка", []))
result.append(new_std_attr("ТочкаМаршрута", "БизнесПроцессТочкаМаршрутаСсылка", []))
elif obj_type == "ChartOfAccounts":
result.append(new_std_attr("Вид", "ВидСчета", []))
result.append(new_std_attr("Забалансовый", "Булево", []))
result.append(new_std_attr("Порядок", "Число", []))
elif obj_type == "ChartOfCalculationTypes":
result.append(new_std_attr("ПериодДействияБазовый", "Булево", []))
# Предопределённые данные есть у всех перечисленных типов, кроме документов и задач
if obj_type in ("Catalog", "ChartOfCharacteristicTypes", "ChartOfAccounts", "ChartOfCalculationTypes"):
result.append(new_std_attr("Предопределенный", "Булево", []))
result.append(new_std_attr("ИмяПредопределенныхДанных", "Строка", []))
return result
def get_attributes(parent_node, child_tag="Attribute", is_dimension=False): def get_attributes(parent_node, child_tag="Attribute", is_dimension=False):
result = [] result = []
for attr in find_all(parent_node, f"md:{child_tag}"): for attr in find_all(parent_node, f"md:{child_tag}"):
@@ -381,7 +602,10 @@ def get_max_name_len(attrs):
def get_simple_children(parent_node, tag): def get_simple_children(parent_node, tag):
result = [] result = []
for child in find_all(parent_node, f"md:{tag}"): for child in find_all(parent_node, f"md:{tag}"):
result.append(inner_text(child)) # Form/Template в ChildObjects — простые узлы с именем в тексте, а Command — узел
# с вложенным Properties: у него inner_text склеил бы всё содержимое в одну строку.
name_node = find(child, "md:Properties/md:Name")
result.append(inner_text(name_node) if name_node is not None else inner_text(child))
return result return result
@@ -472,7 +696,7 @@ def get_ws_operations(child_objs):
# ── Support status of this object (Ext/ParentConfigurations.bin) ── # ── Support status of this object (Ext/ParentConfigurations.bin) ──
# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the # See docs/1c-support-state-spec.md. Walks up to the config root, decodes the
# object's support rule. Never throws — degrades to "не на поддержке". # object's support rule. Never throws — degrades to "не на поддержке".
def _meta_is_external_root(xml_path): def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path): if not os.path.isfile(xml_path):
return False return False
try: try:
@@ -487,7 +711,7 @@ def _meta_is_external_root(xml_path):
def get_object_support_status(obj_uuid): def get_object_support_status(obj_uuid):
try: try:
if _meta_is_external_root(object_path): if _sg_is_external_root(object_path):
return None return None
d = os.path.dirname(object_path) d = os.path.dirname(object_path)
bin_path = None bin_path = None
@@ -705,6 +929,44 @@ if drill_name and child_objs is not None:
drill_done = True drill_done = True
break break
# Не нашли среди дочерних объектов — пробуем стандартные реквизиты ниже
# Drill-down по стандартному реквизиту — единственный способ увидеть состав типов целиком,
# когда в сводке он свёрнут в счётчик. Живёт в Properties, а не в ChildObjects, поэтому идёт
# отдельной веткой и работает даже у объектов без ChildObjects.
if drill_name and not drill_done:
std_all = get_standard_attributes(props, md_type, "full") + get_standard_attributes_full(props, md_type, obj_name)
for s in std_all:
if s["Name"] != drill_name:
continue
out(f"Стандартный реквизит: {s['Name']}")
# Состав типов разворачиваем списком: ради этого drill-down и нужен
type_list = []
type_src = find(props, "md:Type") if drill_name == "ТипЗначения" else None
if type_src is not None:
for t in find_all(type_src, "v8:Type"):
type_list.append(format_single_type(inner_text(t), type_src))
for t in find_all(type_src, "v8:TypeSet"):
type_list.append(format_single_type_set(inner_text(t)))
elif drill_name == "Владелец":
owners_node = find(props, "md:Owners")
if owners_node is not None:
for it in find_all(owners_node, "xr:Item"):
type_list.append(format_md_object_ref(inner_text(it)))
if type_list:
out(f" Типы ({len(type_list)}):")
for t in type_list:
out(f" {t}")
else:
out(f" Тип: {s['Type']}")
flag_text = s["Flags"].strip()
if flag_text:
out(f" Свойства: {flag_text.strip('[]')}")
drill_done = True
break
if not drill_done: if not drill_done:
print(f"[ERROR] '{drill_name}' not found in {obj_name}") print(f"[ERROR] '{drill_name}' not found in {obj_name}")
sys.exit(1) sys.exit(1)
@@ -736,6 +998,18 @@ if not drill_done:
out(f"Расширенное представление списка: {ext_list_presentation}") out(f"Расширенное представление списка: {ext_list_presentation}")
if mode == "brief": if mode == "brief":
# Подчинённость — единственный факт структуры, который из brief не выводится никак,
# а без него код записи элемента падает. Про обязательность здесь намеренно молчим:
# обязательными бывают и обычные реквизиты, а их brief не разбирает.
if md_type == "Catalog":
owners_node = find(props, "md:Owners")
owner_names = []
if owners_node is not None:
for it in find_all(owners_node, "xr:Item"):
owner_names.append(re.sub(r'^\w+\.', '', inner_text(it)))
if owner_names:
out(f"Подчинён: {', '.join(owner_names)}")
# Attributes # Attributes
attrs = get_attributes(child_objs) if child_objs is not None else [] attrs = get_attributes(child_objs) if child_objs is not None else []
if attrs: if attrs:
@@ -865,48 +1139,39 @@ if not drill_done:
# Document-specific header # Document-specific header
if md_type == "Document": if md_type == "Document":
num_type = find(props, "md:NumberType")
num_len = find(props, "md:NumberLength")
num_per = find(props, "md:NumberPeriodicity")
auto_num = find(props, "md:Autonumbering")
posting = find(props, "md:Posting") posting = find(props, "md:Posting")
parts = [] parts = []
if num_type is not None and num_len is not None: # Номер уехал в блок стандартных реквизитов — здесь остаются свойства объекта
nt = "Строка" if inner_text(num_type) == "String" else "Число"
piece = f"Номер: {nt}({inner_text(num_len)})"
if num_per is not None:
per_ru = number_period_map.get(inner_text(num_per), inner_text(num_per))
piece += f", {per_ru}"
if auto_num is not None and inner_text(auto_num) == "true":
piece += ", авто"
parts.append(piece)
if posting is not None: if posting is not None:
parts.append(f"Проведение: {'да' if inner_text(posting) == 'Allow' else 'нет'}") parts.append(f"Проведение: {'да' if inner_text(posting) == 'Allow' else 'нет'}")
if parts: if parts:
out(" | ".join(parts)) out(" | ".join(parts))
# Catalog-specific header # Свойства иерархии и подчинения. Иерархия — не только у справочников: свойство
# Hierarchical есть и у ПВХ (в корпусе acc/erp/ut/unf их 13), а без этой строки
# в full появлялся Родитель, происхождение которого было ниоткуда не видно.
parts = []
hier = find(props, "md:Hierarchical")
if hier is not None and inner_text(hier) == "true":
ht = find(props, "md:HierarchyType")
ht_text = "группы и элементы" if ht is not None and inner_text(ht) == "HierarchyFoldersAndItems" else "элементы"
limit_node = find(props, "md:LimitLevelCount")
level_node = find(props, "md:LevelCount")
if limit_node is not None and inner_text(limit_node) == "true" and level_node is not None:
ht_text += f", уровней: {inner_text(level_node)}"
else:
ht_text += ", без ограничения уровней"
parts.append(f"Иерархический: {ht_text}")
# Код и Наименование уехали в блок стандартных реквизитов — здесь только свойства объекта.
# Подчинение печатаем лишь когда оно отличается от дефолта ToItems.
if md_type == "Catalog": if md_type == "Catalog":
parts = [] sub = find(props, "md:SubordinationUse")
hier = find(props, "md:Hierarchical") if sub is not None and inner_text(sub) != "ToItems":
if hier is not None and inner_text(hier) == "true": sub_ru = {"ToFolders": "группам", "ToFoldersAndItems": "группам и элементам"}.get(
ht = find(props, "md:HierarchyType") inner_text(sub), inner_text(sub))
ht_text = "группы и элементы" if ht is not None and inner_text(ht) == "HierarchyFoldersAndItems" else "элементы" parts.append(f"Подчинение: {sub_ru}")
limit_node = find(props, "md:LimitLevelCount") if parts:
level_node = find(props, "md:LevelCount") out(" | ".join(parts))
if limit_node is not None and inner_text(limit_node) == "true" and level_node is not None:
ht_text += f", уровней: {inner_text(level_node)}"
else:
ht_text += ", без ограничения уровней"
parts.append(f"Иерархический: {ht_text}")
code_len = find(props, "md:CodeLength")
desc_len = find(props, "md:DescriptionLength")
if code_len is not None and inner_text(code_len).isdigit() and int(inner_text(code_len)) > 0:
parts.append(f"Код({inner_text(code_len)})")
if desc_len is not None and inner_text(desc_len).isdigit() and int(inner_text(desc_len)) > 0:
parts.append(f"Наименование({inner_text(desc_len)})")
if parts:
out(" | ".join(parts))
# Register-specific header # Register-specific header
if md_type.endswith("Register"): if md_type.endswith("Register"):
@@ -1067,6 +1332,17 @@ if not drill_done:
syn_text = f'"{v["Synonym"]}"' if v["Synonym"] and v["Synonym"] != v["Name"] else "" syn_text = f'"{v["Synonym"]}"' if v["Synonym"] and v["Synonym"] != v["Name"] else ""
out(f" {padded} {syn_text}") out(f" {padded} {syn_text}")
# Standard attributes
std_attrs = get_standard_attributes(props, md_type, mode)
if mode == "full":
std_attrs += get_standard_attributes_full(props, md_type, obj_name)
if std_attrs:
out("")
out("Стандартные реквизиты:")
ml = get_max_name_len(std_attrs)
for s in std_attrs:
out(format_attr_line(s, ml))
# Dimensions (registers) # Dimensions (registers)
if md_type.endswith("Register") and child_objs is not None: if md_type.endswith("Register") and child_objs is not None:
dims = get_attributes(child_objs, "Dimension", True) dims = get_attributes(child_objs, "Dimension", True)
@@ -1174,6 +1450,12 @@ if not drill_done:
if commands: if commands:
out(f"Команды: {', '.join(commands)}") out(f"Команды: {', '.join(commands)}")
# Единственная подсказка на весь вывод — чем развернуть свёрнутый в счётчик состав
if collapsed_names:
hints = ", ".join(f"-Name {n}" for n in collapsed_names)
out("")
out(f"Полный состав типов: {hints}")
# ── Pagination and output ──────────────────────────────────── # ── Pagination and output ────────────────────────────────────
total_lines = len(lines) total_lines = len(lines)
@@ -1,4 +1,4 @@
# meta-remove v1.5 — Remove metadata object from 1C configuration dump # meta-remove v1.9 — Remove metadata object from 1C configuration dump
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -493,9 +493,29 @@ if (-not $cfgNode) {
# Save Configuration.xml # Save Configuration.xml
if ($actions -gt 0 -and -not $DryRun) { if ($actions -gt 0 -and -not $DryRun) {
$enc = New-Object System.Text.UTF8Encoding $true $enc = New-Object System.Text.UTF8Encoding $true
$sw = New-Object System.IO.StreamWriter($configXml, $false, $enc) # Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$xmlDoc.Save($sw) $settings = New-Object System.Xml.XmlWriterSettings
$sw.Close() $settings.Encoding = $enc
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$xmlDoc.Save($writer)
$writer.Flush(); $writer.Close()
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $configXml) -and ([System.IO.File]::ReadAllText($configXml) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($configXml, $xmlText, $enc)
Write-Host "[OK] Configuration.xml saved" Write-Host "[OK] Configuration.xml saved"
} }
} }
@@ -559,9 +579,29 @@ function Remove-FromSubsystems {
if ($modified -and -not $DryRun) { if ($modified -and -not $DryRun) {
$enc = New-Object System.Text.UTF8Encoding $true $enc = New-Object System.Text.UTF8Encoding $true
$sw = New-Object System.IO.StreamWriter($xmlFile.FullName, $false, $enc) # Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$ssDoc.Save($sw) $settings = New-Object System.Xml.XmlWriterSettings
$sw.Close() $settings.Encoding = $enc
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$ssDoc.Save($writer)
$writer.Flush(); $writer.Close()
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $xmlFile.FullName) -and ([System.IO.File]::ReadAllText($xmlFile.FullName) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($xmlFile.FullName, $xmlText, $enc)
} }
# Recurse into child subsystems # Recurse into child subsystems
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# meta-remove v1.5 — Remove metadata object from 1C configuration dump # meta-remove v1.9 — Remove metadata object from 1C configuration dump
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -10,6 +10,28 @@ import sys
import shutil import shutil
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================ # ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
@@ -292,21 +314,22 @@ def _detect_xml_style(path):
def _finalize_xml_bytes(xml_bytes, style): def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None).""" """Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
enc_decl = style["enc"] if style else "utf-8" выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
enc_decl = style["enc"] if style else "UTF-8"
xml_bytes = xml_bytes.replace( xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>", b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>') b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах) # Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"") xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n")) .replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть) # Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else True want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n") xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl: if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение) # EOL — как в оригинале (новый файл → CRLF, канон #57)
if style and style["crlf"]: if (style["crlf"] if style else True):
xml_bytes = xml_bytes.replace(b"\n", b"\r\n") xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes return xml_bytes
@@ -330,7 +353,7 @@ def main():
parser.add_argument("-DryRun", action="store_true") parser.add_argument("-DryRun", action="store_true")
parser.add_argument("-KeepFiles", action="store_true") parser.add_argument("-KeepFiles", action="store_true")
parser.add_argument("-Force", action="store_true") parser.add_argument("-Force", action="store_true")
args = parser.parse_args() args = ci_parse_args(parser)
config_dir = args.ConfigDir config_dir = args.ConfigDir
if not os.path.isabs(config_dir): if not os.path.isabs(config_dir):
@@ -1,4 +1,4 @@
# meta-validate v1.13 — Validate 1C metadata object structure (+корневой <Type>: скаляр без структуры = ошибка) # meta-validate v1.18 — Validate 1C metadata object structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -209,7 +209,7 @@ $standardAttributesByType = @{
"Enum" = @("Order","Ref") "Enum" = @("Order","Ref")
"InformationRegister" = @("Active","LineNumber","Recorder","Period") "InformationRegister" = @("Active","LineNumber","Recorder","Period")
"AccumulationRegister" = @("Active","LineNumber","Recorder","Period","RecordType") "AccumulationRegister" = @("Active","LineNumber","Recorder","Period","RecordType")
"AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account") "AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account","PeriodAdjustment","RecordType")
"CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry","ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod") "CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry","ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod")
"ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance") "ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance")
"ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","IsFolder","ValueType") "ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","IsFolder","ValueType")
@@ -220,6 +220,14 @@ $standardAttributesByType = @{
"DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number") "DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number")
} }
# Стандартные реквизиты, присутствие которых зависит от свойств объекта: у бухрегистра
# PeriodAdjustment — от длины периода корректировки, RecordType — от корреспонденции; у регистра
# накопления RecordType — от вида регистра. Их отсутствие законно, в «Missing» не попадают.
$stdAttrConditionalNames = @{
"AccountingRegister" = @("PeriodAdjustment","RecordType")
"AccumulationRegister" = @("RecordType")
}
# Types that have StandardAttributes block # Types that have StandardAttributes block
$typesWithStdAttrs = @( $typesWithStdAttrs = @(
"Catalog","Document","Enum", "Catalog","Document","Enum",
@@ -279,7 +287,7 @@ $validPropertyValues = @{
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition") "InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
"RegisterType" = @("Balance","Turnovers") "RegisterType" = @("Balance","Turnovers")
"ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession") "ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession")
"ReuseSessions" = @("DontUse","AutoUse") "ReuseSessions" = @("DontUse","Use","AutoUse")
"FillChecking" = @("DontCheck","ShowError","ShowWarning") "FillChecking" = @("DontCheck","ShowError","ShowWarning")
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
"DataHistory" = @("Use","DontUse") "DataHistory" = @("Use","DontUse")
@@ -344,9 +352,10 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version") $version = $root.GetAttribute("version")
if (-not $version) { if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject" Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20")) { } elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). # Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26),
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20)" # 2.20 (8.3.27), 2.21 (8.5). Версию задаёт платформа ВЫГРУЗКИ, а не режим совместимости.
Report-Warn "1. Unusual version '$version' (expected 2.17-2.21)"
} }
# Detect type element — exactly one child element in md namespace # Detect type element — exactly one child element in md namespace
@@ -606,11 +615,9 @@ if ($typesWithStdAttrs -contains $mdType) {
if ($saName) { if ($saName) {
$foundNames += $saName $foundNames += $saName
if ($expectedStdAttrs -notcontains $saName) { if ($expectedStdAttrs -notcontains $saName) {
# AccountingRegister has dynamic ExtDimension{N}/ExtDimensionType{N} and optional PeriodAdjustment # AccountingRegister: пары субконто, число которых задаётся планом счетов
$isDynamic = ($mdType -eq "AccountingRegister" -and ($saName -match '^ExtDimension\d+$' -or $saName -match '^ExtDimensionType\d+$' -or $saName -eq "PeriodAdjustment")) $isDynamic = ($mdType -eq "AccountingRegister" -and ($saName -match '^ExtDimension\d+$' -or $saName -match '^ExtDimensionType\d+$'))
# CalculationRegister has conditional period attrs if (-not $isDynamic) {
$isCalcDynamic = ($mdType -eq "CalculationRegister" -and $saName -in @("ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod"))
if (-not $isDynamic -and -not $isCalcDynamic) {
Report-Warn "5. Unexpected StandardAttribute '$saName' for $mdType" Report-Warn "5. Unexpected StandardAttribute '$saName' for $mdType"
} }
} }
@@ -621,7 +628,8 @@ if ($typesWithStdAttrs -contains $mdType) {
} }
if ($expectedStdAttrs) { if ($expectedStdAttrs) {
$missingAttrs = @($expectedStdAttrs | Where-Object { $foundNames -notcontains $_ }) $condNames = $stdAttrConditionalNames[$mdType]; if (-not $condNames) { $condNames = @() }
$missingAttrs = @($expectedStdAttrs | Where-Object { $foundNames -notcontains $_ -and $condNames -notcontains $_ })
if ($missingAttrs.Count -gt 0) { if ($missingAttrs.Count -gt 0) {
Report-Warn "5. Missing StandardAttributes: $($missingAttrs -join ', ')" Report-Warn "5. Missing StandardAttributes: $($missingAttrs -join ', ')"
} }
@@ -1502,10 +1510,14 @@ if ($script:configDir) {
$versionedProps = @{ $versionedProps = @{
"TypeReductionMode" = "2.18" # режим приведения типов (стандартные реквизиты, измерения РС) "TypeReductionMode" = "2.18" # режим приведения типов (стандартные реквизиты, измерения РС)
"LineNumberLength" = "2.20" # длина номера строки ТЧ (5..9) "LineNumberLength" = "2.20" # длина номера строки ТЧ (5..9)
# 2.21 (8.5): подтверждено синтетикой — одни исходники, выгрузка с 8.3.27 и с 8.5.1.
"Color" = "2.21" # цвет значения перечисления
"AuxiliaryVariantForm" = "2.21" # вспомогательная форма варианта отчёта
"UseInInterfaceCompatibilityMode" = "2.21" # использование общей формы в режиме совместимости интерфейса
} }
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17"). # Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$v) { function Get-FormatRank([string]$ver) {
if ($v -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] } if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0 return 0
} }
$fileRank = Get-FormatRank $version $fileRank = Get-FormatRank $version
@@ -1,4 +1,4 @@
# meta-validate v1.13 — Validate 1C metadata object structure (Python port) (+корневой <Type>: скаляр без структуры = ошибка) # meta-validate v1.18 — Validate 1C metadata object structure (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import os import os
@@ -11,6 +11,28 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ── arg parsing ────────────────────────────────────────────── # ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False) parser = argparse.ArgumentParser(allow_abbrev=False)
@@ -18,7 +40,7 @@ parser.add_argument("-ObjectPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true") parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30) parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default="") parser.add_argument("-OutFile", default="")
args = parser.parse_args() args = ci_parse_args(parser)
detailed = args.Detailed detailed = args.Detailed
max_errors = args.MaxErrors max_errors = args.MaxErrors
@@ -209,7 +231,7 @@ standard_attributes_by_type = {
"Enum": ["Order", "Ref"], "Enum": ["Order", "Ref"],
"InformationRegister": ["Active", "LineNumber", "Recorder", "Period"], "InformationRegister": ["Active", "LineNumber", "Recorder", "Period"],
"AccumulationRegister": ["Active", "LineNumber", "Recorder", "Period", "RecordType"], "AccumulationRegister": ["Active", "LineNumber", "Recorder", "Period", "RecordType"],
"AccountingRegister": ["Active", "Period", "Recorder", "LineNumber", "Account"], "AccountingRegister": ["Active", "Period", "Recorder", "LineNumber", "Account", "PeriodAdjustment", "RecordType"],
"CalculationRegister": ["Active", "Recorder", "LineNumber", "RegistrationPeriod", "CalculationType", "ReversingEntry", "ActionPeriod", "BegOfActionPeriod", "EndOfActionPeriod", "BegOfBasePeriod", "EndOfBasePeriod"], "CalculationRegister": ["Active", "Recorder", "LineNumber", "RegistrationPeriod", "CalculationType", "ReversingEntry", "ActionPeriod", "BegOfActionPeriod", "EndOfActionPeriod", "BegOfBasePeriod", "EndOfBasePeriod"],
"ChartOfAccounts": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "Order", "Type", "OffBalance"], "ChartOfAccounts": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "Order", "Type", "OffBalance"],
"ChartOfCharacteristicTypes": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "IsFolder", "ValueType"], "ChartOfCharacteristicTypes": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "IsFolder", "ValueType"],
@@ -220,6 +242,14 @@ standard_attributes_by_type = {
"DocumentJournal": ["Type", "Ref", "Date", "Posted", "DeletionMark", "Number"], "DocumentJournal": ["Type", "Ref", "Date", "Posted", "DeletionMark", "Number"],
} }
# Стандартные реквизиты, присутствие которых зависит от свойств объекта: у бухрегистра
# PeriodAdjustment — от длины периода корректировки, RecordType — от корреспонденции; у регистра
# накопления RecordType — от вида регистра. Их отсутствие законно, в «Missing» не попадают.
std_attr_conditional_names = {
"AccountingRegister": ("PeriodAdjustment", "RecordType"),
"AccumulationRegister": ("RecordType",),
}
# Types that have StandardAttributes block # Types that have StandardAttributes block
types_with_std_attrs = ( types_with_std_attrs = (
"Catalog", "Document", "Enum", "Catalog", "Document", "Enum",
@@ -281,7 +311,7 @@ valid_property_values = {
"InformationRegisterPeriodicity": ["Nonperiodical", "Second", "Day", "Month", "Quarter", "Year", "RecorderPosition"], "InformationRegisterPeriodicity": ["Nonperiodical", "Second", "Day", "Month", "Quarter", "Year", "RecorderPosition"],
"RegisterType": ["Balance", "Turnovers"], "RegisterType": ["Balance", "Turnovers"],
"ReturnValuesReuse": ["DontUse", "DuringRequest", "DuringSession"], "ReturnValuesReuse": ["DontUse", "DuringRequest", "DuringSession"],
"ReuseSessions": ["DontUse", "AutoUse"], "ReuseSessions": ["DontUse", "Use", "AutoUse"],
"FillChecking": ["DontCheck", "ShowError", "ShowWarning"], "FillChecking": ["DontCheck", "ShowError", "ShowWarning"],
"Indexing": ["DontIndex", "Index", "IndexWithAdditionalOrder"], "Indexing": ["DontIndex", "Index", "IndexWithAdditionalOrder"],
"DataHistory": ["Use", "DontUse"], "DataHistory": ["Use", "DontUse"],
@@ -371,9 +401,10 @@ if root_ns != expected_ns:
version = root.get("version", "") version = root.get("version", "")
if not version: if not version:
report_warn("1. Missing version attribute on MetaDataObject") report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.18", "2.19", "2.20"): elif version not in ("2.17", "2.18", "2.19", "2.20", "2.21"):
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). # Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26),
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.20)") # 2.20 (8.3.27), 2.21 (8.5). Версию задаёт платформа ВЫГРУЗКИ, а не режим совместимости.
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.21)")
# Detect type element -- exactly one child element in md namespace # Detect type element -- exactly one child element in md namespace
type_node = None type_node = None
@@ -594,23 +625,19 @@ if md_type in types_with_std_attrs:
if sa_name: if sa_name:
found_names.append(sa_name) found_names.append(sa_name)
if sa_name not in expected_std_attrs: if sa_name not in expected_std_attrs:
# AccountingRegister has dynamic attrs # AccountingRegister: пары субконто, число которых задаётся планом счетов
is_dynamic = (md_type == "AccountingRegister" and is_dynamic = (md_type == "AccountingRegister" and
(re.match(r'^ExtDimension\d+$', sa_name) or (re.match(r'^ExtDimension\d+$', sa_name) or
re.match(r'^ExtDimensionType\d+$', sa_name) or re.match(r'^ExtDimensionType\d+$', sa_name)))
sa_name == "PeriodAdjustment")) if not is_dynamic:
# CalculationRegister has conditional period attrs
is_calc_dynamic = (md_type == "CalculationRegister" and
sa_name in ("ActionPeriod", "BegOfActionPeriod", "EndOfActionPeriod",
"BegOfBasePeriod", "EndOfBasePeriod"))
if not is_dynamic and not is_calc_dynamic:
report_warn(f"5. Unexpected StandardAttribute '{sa_name}' for {md_type}") report_warn(f"5. Unexpected StandardAttribute '{sa_name}' for {md_type}")
else: else:
report_error("5. StandardAttribute without 'name' attribute") report_error("5. StandardAttribute without 'name' attribute")
check5_ok = False check5_ok = False
if expected_std_attrs: if expected_std_attrs:
missing_attrs = [a for a in expected_std_attrs if a not in found_names] cond_names = std_attr_conditional_names.get(md_type, ())
missing_attrs = [a for a in expected_std_attrs if a not in found_names and a not in cond_names]
if missing_attrs: if missing_attrs:
report_warn(f"5. Missing StandardAttributes: {', '.join(missing_attrs)}") report_warn(f"5. Missing StandardAttributes: {', '.join(missing_attrs)}")
@@ -1404,12 +1431,16 @@ if config_dir:
versioned_props = { versioned_props = {
"TypeReductionMode": "2.18", # режим приведения типов (стандартные реквизиты, измерения РС) "TypeReductionMode": "2.18", # режим приведения типов (стандартные реквизиты, измерения РС)
"LineNumberLength": "2.20", # длина номера строки ТЧ (5..9) "LineNumberLength": "2.20", # длина номера строки ТЧ (5..9)
# 2.21 (8.5): подтверждено синтетикой — одни исходники, выгрузка с 8.3.27 и с 8.5.1.
"Color": "2.21", # цвет значения перечисления
"AuxiliaryVariantForm": "2.21", # вспомогательная форма варианта отчёта
"UseInInterfaceCompatibilityMode": "2.21", # использование общей формы в режиме совместимости интерфейса
} }
def format_rank(v): def format_rank(ver):
""""2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").""" """"2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', v or '') m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0 return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
@@ -1,4 +1,4 @@
# mxl-compile v1.5 — Compile 1C spreadsheet from JSON # mxl-compile v1.14 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -142,6 +142,46 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
} catch { return } } catch { return }
} }
# --- Detect XML format version ---
# У корня <document> нет атрибута version, поэтому версию берём из конфигурации, в дерево
# которой пишем макет. Вне конфигурации (автономный .xml, исходники EPF) остаётся 2.17.
function Detect-FormatVersion([string]$dir) {
$d = $dir
while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
if ($parent -eq $d) { break }
$d = $parent
}
return "2.17"
}
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved))
# --- 1. Load and validate JSON --- # --- 1. Load and validate JSON ---
if (-not (Test-Path $JsonPath)) { if (-not (Test-Path $JsonPath)) {
@@ -437,6 +477,12 @@ foreach ($col in ($colWidthMap.Keys | Sort-Object)) {
# Helper: escape XML special characters # Helper: escape XML special characters
function Esc-Xml { function Esc-Xml {
param([string]$s)
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
function Esc-XmlText {
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует — # Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной &quot;). &quot; платформа # пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной &quot;). &quot; платформа
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе. # принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
@@ -512,8 +558,14 @@ function X {
} }
# 7a. Header # 7a. Header
$docNsDecl = 'xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (перед style):
# платформа держит объявления по алфавиту, дописать в конец нельзя.
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$docNsDecl = $docNsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
X '<?xml version="1.0" encoding="UTF-8"?>' X '<?xml version="1.0" encoding="UTF-8"?>'
X '<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' X "<document $docNsDecl>"
# 7b. Language settings # 7b. Language settings
X "`t<languageSettings>" X "`t<languageSettings>"
@@ -714,7 +766,7 @@ foreach ($area in $def.areas) {
X "`t`t`t`t`t<tl>" X "`t`t`t`t`t<tl>"
X "`t`t`t`t`t`t<v8:item>" X "`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>" X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t`t`t<v8:content>$(Esc-Xml $cellInfo.Text)</v8:content>" X "`t`t`t`t`t`t`t<v8:content>$(Esc-XmlText $cellInfo.Text)</v8:content>"
X "`t`t`t`t`t`t</v8:item>" X "`t`t`t`t`t`t</v8:item>"
X "`t`t`t`t`t</tl>" X "`t`t`t`t`t</tl>"
} }
@@ -723,7 +775,7 @@ foreach ($area in $def.areas) {
X "`t`t`t`t`t<tl>" X "`t`t`t`t`t<tl>"
X "`t`t`t`t`t`t<v8:item>" X "`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>" X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t`t`t<v8:content>$(Esc-Xml $cellInfo.Template)</v8:content>" X "`t`t`t`t`t`t`t<v8:content>$(Esc-XmlText $cellInfo.Template)</v8:content>"
X "`t`t`t`t`t`t</v8:item>" X "`t`t`t`t`t`t</v8:item>"
X "`t`t`t`t`t</tl>" X "`t`t`t`t`t</tl>"
} }
@@ -839,7 +891,7 @@ foreach ($key in $formatRegistry.Keys) {
X "`t`t<format>" X "`t`t<format>"
X "`t`t`t<v8:item>" X "`t`t`t<v8:item>"
X "`t`t`t`t<v8:lang>ru</v8:lang>" X "`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t<v8:content>$(Esc-Xml $fmt.NumberFormat)</v8:content>" X "`t`t`t`t<v8:content>$(Esc-XmlText $fmt.NumberFormat)</v8:content>"
X "`t`t`t</v8:item>" X "`t`t`t</v8:item>"
X "`t`t</format>" X "`t`t</format>"
} }
@@ -854,8 +906,13 @@ X '</document>'
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
$resolvedPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } $resolvedPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
# Каталог назначения создаём сами: типовой путь — Templates/<Имя>/Ext/Template.xml,
# и его может ещё не быть. Так делают и form-compile, и skd-compile, и py-порт этого
# навыка; без этого PS-порт падал на «Could not find a part of the path».
$outDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
if ($outDir -and -not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null }
Assert-EditAllowed $resolvedPath 'editable' Assert-EditAllowed $resolvedPath 'editable'
[System.IO.File]::WriteAllText($resolvedPath, $xml.ToString(), $enc) [System.IO.File]::WriteAllText($resolvedPath, $xml.ToString().TrimEnd("`r", "`n"), $enc)
# --- 9. Summary --- # --- 9. Summary ---
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# mxl-compile v1.5 — Compile 1C spreadsheet from JSON # mxl-compile v1.14 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import json import json
@@ -10,6 +10,65 @@ import sys
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================ # ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
@@ -188,23 +247,69 @@ def assert_edit_allowed(target_path, require):
def esc_xml(s): def esc_xml(s):
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def esc_xml_text(s):
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует """Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
(92142 сырых кавычки на корпус, ни одной &quot;); &quot; она принимает, но нормализует обратно.""" (92142 сырых кавычки на корпус, ни одной &quot;); &quot; она принимает, но нормализует обратно."""
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;') return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def write_utf8_bom(path, content): def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
def detect_format_version(d):
while d:
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
ext_path = d + ".xml"
if os.path.isfile(ext_path):
with open(ext_path, "r", encoding="utf-8-sig") as f:
ext_head = f.read(2000)
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
if m:
return m.group(1)
cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f:
head = f.read(2000)
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
if m:
return m.group(1)
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return "2.17"
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Compile 1C spreadsheet from JSON', allow_abbrev=False) parser = argparse.ArgumentParser(description='Compile 1C spreadsheet from JSON', allow_abbrev=False)
parser.add_argument('-JsonPath', type=str, required=True) parser.add_argument('-JsonPath', type=str, required=True)
parser.add_argument('-OutputPath', type=str, required=True) parser.add_argument('-OutputPath', type=str, required=True)
args = parser.parse_args() args = ci_parse_args(parser)
# --- Detect XML format version ---
# У корня <document> нет атрибута version, поэтому версию берём из конфигурации, в дерево
# которой пишем макет. Вне конфигурации (автономный .xml, исходники EPF) остаётся 2.17.
out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath)
format_version = detect_format_version(os.path.dirname(out_path_resolved))
# --- 1. Load and validate JSON --- # --- 1. Load and validate JSON ---
json_path = args.JsonPath json_path = args.JsonPath
@@ -213,7 +318,7 @@ def main():
sys.exit(1) sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f: with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.load(f) defn = ci_json(json.load(f))
if not defn.get('columns'): if not defn.get('columns'):
print("Required field 'columns' is missing", file=sys.stderr) print("Required field 'columns' is missing", file=sys.stderr)
@@ -496,7 +601,16 @@ def main():
# 7a. Header # 7a. Header
lines.append('<?xml version="1.0" encoding="UTF-8"?>') lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">') doc_ns_decl = ('xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (перед style):
# платформа держит объявления по алфавиту, дописать в конец нельзя.
if format_rank(format_version) >= 221:
doc_ns_decl = doc_ns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
lines.append(f'<document {doc_ns_decl}>')
# 7b. Language settings # 7b. Language settings
lines.append('\t<languageSettings>') lines.append('\t<languageSettings>')
@@ -678,7 +792,7 @@ def main():
lines.append('\t\t\t\t\t<tl>') lines.append('\t\t\t\t\t<tl>')
lines.append('\t\t\t\t\t\t<v8:item>') lines.append('\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>') lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_info["Text"])}</v8:content>') lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml_text(cell_info["Text"])}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>') lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>') lines.append('\t\t\t\t\t</tl>')
@@ -686,7 +800,7 @@ def main():
lines.append('\t\t\t\t\t<tl>') lines.append('\t\t\t\t\t<tl>')
lines.append('\t\t\t\t\t\t<v8:item>') lines.append('\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>') lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_info["Template"])}</v8:content>') lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml_text(cell_info["Template"])}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>') lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>') lines.append('\t\t\t\t\t</tl>')
@@ -782,7 +896,7 @@ def main():
lines.append('\t\t<format>') lines.append('\t\t<format>')
lines.append('\t\t\t<v8:item>') lines.append('\t\t\t<v8:item>')
lines.append('\t\t\t\t<v8:lang>ru</v8:lang>') lines.append('\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t<v8:content>{esc_xml(fmt["NumberFormat"])}</v8:content>') lines.append(f'\t\t\t\t<v8:content>{esc_xml_text(fmt["NumberFormat"])}</v8:content>')
lines.append('\t\t\t</v8:item>') lines.append('\t\t\t</v8:item>')
lines.append('\t\t</format>') lines.append('\t\t</format>')
@@ -802,7 +916,7 @@ def main():
if out_dir and not os.path.exists(out_dir): if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir, exist_ok=True) os.makedirs(out_dir, exist_ok=True)
content = '\n'.join(lines) + '\n' content = '\r\n'.join(lines)
write_utf8_bom(out_path, content) write_utf8_bom(out_path, content)
# --- 9. Summary --- # --- 9. Summary ---
@@ -1,4 +1,4 @@
# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON # mxl-decompile v1.1 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON # mxl-decompile v1.1 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -9,6 +9,28 @@ import sys
from collections import OrderedDict from collections import OrderedDict
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Namespace map --- # --- Namespace map ---
NSMAP = { NSMAP = {
@@ -49,7 +71,7 @@ def main():
parser = argparse.ArgumentParser(description="Decompile 1C spreadsheet to JSON", allow_abbrev=False) parser = argparse.ArgumentParser(description="Decompile 1C spreadsheet to JSON", allow_abbrev=False)
parser.add_argument("-TemplatePath", "-Path", required=True, help="Path to Template.xml") parser.add_argument("-TemplatePath", "-Path", required=True, help="Path to Template.xml")
parser.add_argument("-OutputPath", default=None, help="Output JSON path (stdout if omitted)") parser.add_argument("-OutputPath", default=None, help="Output JSON path (stdout if omitted)")
args = parser.parse_args() args = ci_parse_args(parser)
template_path = args.TemplatePath template_path = args.TemplatePath
output_path = args.OutputPath output_path = args.OutputPath
+1 -1
View File
@@ -1,4 +1,4 @@
# mxl-info v1.2 — Analyze 1C spreadsheet structure # mxl-info v1.4 — Analyze 1C spreadsheet structure (+единое имя хелпера состояния поддержки)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Alias('Path')] [Alias('Path')]
+27 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# mxl-info v1.2 — Analyze 1C spreadsheet structure # mxl-info v1.4 — Analyze 1C spreadsheet structure (+единое имя хелпера состояния поддержки)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -12,6 +12,28 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Argument parsing --- # --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C spreadsheet (MXL) structure", allow_abbrev=False) 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("-TemplatePath", "-Path", default="", help="Path to Template.xml")
@@ -23,7 +45,7 @@ parser.add_argument("-WithText", action="store_true", default=False, help="Inclu
parser.add_argument("-MaxParams", type=int, default=10, help="Max parameters to show per area") 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("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip") parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
args = parser.parse_args() args = ci_parse_args(parser)
# --- Resolve template path --- # --- Resolve template path ---
template_path = args.TemplatePath template_path = args.TemplatePath
@@ -321,7 +343,7 @@ def get_support_status_for_path(target_path):
except Exception: except Exception:
pass pass
return None return None
def is_external_root(xml_path): def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path): if not os.path.isfile(xml_path):
return False return False
try: try:
@@ -335,14 +357,14 @@ def get_support_status_for_path(target_path):
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
elem_uuid = root_uuid(rp) elem_uuid = root_uuid(rp)
if is_external_root(rp): if _sg_is_external_root(rp):
return None return None
bin_path = None bin_path = None
d = os.path.dirname(rp) d = os.path.dirname(rp)
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if is_external_root(d + ".xml"): if _sg_is_external_root(d + ".xml"):
return None return None
if not elem_uuid: if not elem_uuid:
elem_uuid = root_uuid(d + ".xml") elem_uuid = root_uuid(d + ".xml")
@@ -1,4 +1,4 @@
# mxl-validate v1.1 — Validate 1C spreadsheet # mxl-validate v1.2 — Validate 1C spreadsheet
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Alias('Path')] [Alias('Path')]
@@ -1,10 +1,32 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# mxl-validate v1.1 — Validate 1C spreadsheet document Template.xml # mxl-validate v1.2 — Validate 1C spreadsheet document Template.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates spreadsheet Template.xml: height, palette refs, column/row indices, areas, merges.""" """Validates spreadsheet Template.xml: height, palette refs, column/row indices, areas, merges."""
import sys, os, argparse import sys, os, argparse
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
NS_D = 'http://v8.1c.ru/8.2/data/spreadsheet' NS_D = 'http://v8.1c.ru/8.2/data/spreadsheet'
NS_V8 = 'http://v8.1c.ru/8.1/data/core' NS_V8 = 'http://v8.1c.ru/8.1/data/core'
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance' NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
@@ -61,7 +83,7 @@ def main():
parser.add_argument('-SrcDir', dest='SrcDir', default='src') parser.add_argument('-SrcDir', dest='SrcDir', default='src')
parser.add_argument('-Detailed', action='store_true') parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=20) parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=20)
args = parser.parse_args() args = ci_parse_args(parser)
template_path = args.TemplatePath template_path = args.TemplatePath
processor_name = args.ProcessorName processor_name = args.ProcessorName
@@ -1,4 +1,4 @@
# role-compile v1.10 — Compile 1C role from JSON # role-compile v1.24 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -171,6 +171,12 @@ function X {
} }
function Esc-Xml { function Esc-Xml {
param([string]$s)
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
function Esc-XmlText {
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует — # Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной &quot;). &quot; платформа # пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной &quot;). &quot; платформа
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе. # принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
@@ -187,15 +193,18 @@ $script:typeAliases = @{
"РегистрНакопления" = "AccumulationRegister" "РегистрНакопления" = "AccumulationRegister"
"РегистрБухгалтерии" = "AccountingRegister" "РегистрБухгалтерии" = "AccountingRegister"
"РегистрРасчета" = "CalculationRegister" "РегистрРасчета" = "CalculationRegister"
"РегистрРасчёта" = "CalculationRegister"
"Константа" = "Constant" "Константа" = "Constant"
"ПланСчетов" = "ChartOfAccounts" "ПланСчетов" = "ChartOfAccounts"
"ПланВидовХарактеристик" = "ChartOfCharacteristicTypes" "ПланВидовХарактеристик" = "ChartOfCharacteristicTypes"
"ПланВидовРасчета" = "ChartOfCalculationTypes" "ПланВидовРасчета" = "ChartOfCalculationTypes"
"ПланВидовРасчёта" = "ChartOfCalculationTypes"
"ПланОбмена" = "ExchangePlan" "ПланОбмена" = "ExchangePlan"
"БизнесПроцесс" = "BusinessProcess" "БизнесПроцесс" = "BusinessProcess"
"Задача" = "Task" "Задача" = "Task"
"Обработка" = "DataProcessor" "Обработка" = "DataProcessor"
"Отчет" = "Report" "Отчет" = "Report"
"Отчёт" = "Report"
"ОбщаяФорма" = "CommonForm" "ОбщаяФорма" = "CommonForm"
"ОбщаяКоманда" = "CommonCommand" "ОбщаяКоманда" = "CommonCommand"
"Подсистема" = "Subsystem" "Подсистема" = "Subsystem"
@@ -644,6 +653,14 @@ if ($def.objects) {
function Detect-FormatVersion([string]$dir) { function Detect-FormatVersion([string]$dir) {
$d = $dir $d = $dir
while ($d) { while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml" $cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) { if (Test-Path $cfgPath) {
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8) $cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
@@ -659,7 +676,14 @@ function Detect-FormatVersion([string]$dir) {
return "2.17" return "2.17"
} }
$resolvedOutputDir = if ([System.IO.Path]::IsPathRooted($OutputDir)) { $OutputDir } else { Join-Path (Get-Location) $OutputDir } # Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$resolvedOutputDir =if ([System.IO.Path]::IsPathRooted($OutputDir)) { $OutputDir } else { Join-Path (Get-Location) $OutputDir }
Assert-EditAllowed $resolvedOutputDir 'editable' Assert-EditAllowed $resolvedOutputDir 'editable'
$formatVersion = Detect-FormatVersion $resolvedOutputDir $formatVersion = Detect-FormatVersion $resolvedOutputDir
@@ -672,40 +696,30 @@ $uuid = [guid]::NewGuid().ToString()
$script:xmlBuf = New-Object System.Text.StringBuilder 4096 $script:xmlBuf = New-Object System.Text.StringBuilder 4096
X '<?xml version="1.0" encoding="UTF-8"?>' X '<?xml version="1.0" encoding="UTF-8"?>'
X '<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"' # Объявления пространств имён — одной переменной и одной строкой, как пишет платформа.
X ' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"' $xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
X ' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"' # 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
X ' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"' # платформа держит объявления по алфавиту. В Rights.xml палитра НЕ идёт (проверено по выгрузке 8.5).
X ' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"' if ((Get-FormatRank $formatVersion) -ge 221) {
X ' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"' $xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
X ' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
X ' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
X ' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
X ' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
X ' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
X ' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
X ' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
X ' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
X ' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
X ' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
X ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
X " version=`"$formatVersion`">"
X " <Role uuid=`"$uuid`">"
X ' <Properties>'
X " <Name>$roleName</Name>"
X ' <Synonym>'
X ' <v8:item>'
X ' <v8:lang>ru</v8:lang>'
X " <v8:content>$(Esc-Xml $synonym)</v8:content>"
X ' </v8:item>'
X ' </Synonym>'
if ($comment) {
X " <Comment>$(Esc-Xml $comment)</Comment>"
} else {
X ' <Comment/>'
} }
X ' </Properties>' X "<MetaDataObject $xmlnsDecl version=`"$formatVersion`">"
X ' </Role>' X "`t<Role uuid=`"$uuid`">"
X "`t`t<Properties>"
X "`t`t`t<Name>$roleName</Name>"
X "`t`t`t<Synonym>"
X "`t`t`t`t<v8:item>"
X "`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t<v8:content>$(Esc-XmlText $synonym)</v8:content>"
X "`t`t`t`t</v8:item>"
X "`t`t`t</Synonym>"
if ($comment) {
X "`t`t`t<Comment>$(Esc-XmlText $comment)</Comment>"
} else {
X "`t`t`t<Comment/>"
}
X "`t`t</Properties>"
X "`t</Role>"
X '</MetaDataObject>' X '</MetaDataObject>'
$metadataXml = $script:xmlBuf.ToString() $metadataXml = $script:xmlBuf.ToString()
@@ -715,48 +729,45 @@ $metadataXml = $script:xmlBuf.ToString()
$script:xmlBuf = New-Object System.Text.StringBuilder 8192 $script:xmlBuf = New-Object System.Text.StringBuilder 8192
X '<?xml version="1.0" encoding="UTF-8"?>' X '<?xml version="1.0" encoding="UTF-8"?>'
X '<Rights xmlns="http://v8.1c.ru/8.2/roles"' X "<Rights xmlns=`"http://v8.1c.ru/8.2/roles`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" xsi:type=`"Rights`" version=`"$formatVersion`">"
X ' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
X ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
X " xsi:type=`"Rights`" version=`"$formatVersion`">"
# Global flags (defaults match typical 1C roles) # Global flags (defaults match typical 1C roles)
$sfno = if ($null -ne $def.setForNewObjects) { "$($def.setForNewObjects)".ToLower() } else { "false" } $sfno = if ($null -ne $def.setForNewObjects) { "$($def.setForNewObjects)".ToLower() } else { "false" }
$sfab = if ($null -ne $def.setForAttributesByDefault) { "$($def.setForAttributesByDefault)".ToLower() } else { "true" } $sfab = if ($null -ne $def.setForAttributesByDefault) { "$($def.setForAttributesByDefault)".ToLower() } else { "true" }
$irco = if ($null -ne $def.independentRightsOfChildObjects) { "$($def.independentRightsOfChildObjects)".ToLower() } else { "false" } $irco = if ($null -ne $def.independentRightsOfChildObjects) { "$($def.independentRightsOfChildObjects)".ToLower() } else { "false" }
X " <setForNewObjects>$sfno</setForNewObjects>" X "`t<setForNewObjects>$sfno</setForNewObjects>"
X " <setForAttributesByDefault>$sfab</setForAttributesByDefault>" X "`t<setForAttributesByDefault>$sfab</setForAttributesByDefault>"
X " <independentRightsOfChildObjects>$irco</independentRightsOfChildObjects>" X "`t<independentRightsOfChildObjects>$irco</independentRightsOfChildObjects>"
# Object blocks # Object blocks
$totalRights = 0 $totalRights = 0
foreach ($obj in $parsedObjects) { foreach ($obj in $parsedObjects) {
X ' <object>' X "`t<object>"
X " <name>$($obj.Name)</name>" X "`t`t<name>$($obj.Name)</name>"
foreach ($right in $obj.Rights) { foreach ($right in $obj.Rights) {
X ' <right>' X "`t`t<right>"
X " <name>$($right.Name)</name>" X "`t`t`t<name>$($right.Name)</name>"
X " <value>$($right.Value)</value>" X "`t`t`t<value>$($right.Value)</value>"
if ($right.Condition) { if ($right.Condition) {
X ' <restrictionByCondition>' X "`t`t`t<restrictionByCondition>"
X " <condition>$(Esc-Xml $right.Condition)</condition>" X "`t`t`t`t<condition>$(Esc-XmlText $right.Condition)</condition>"
X ' </restrictionByCondition>' X "`t`t`t</restrictionByCondition>"
} }
X ' </right>' X "`t`t</right>"
$totalRights++ $totalRights++
} }
X ' </object>' X "`t</object>"
} }
# RLS restriction templates # RLS restriction templates
$templateCount = 0 $templateCount = 0
if ($def.templates) { if ($def.templates) {
foreach ($tpl in $def.templates) { foreach ($tpl in $def.templates) {
X ' <restrictionTemplate>' X "`t<restrictionTemplate>"
X " <name>$(Esc-Xml "$($tpl.name)")</name>" X "`t`t<name>$(Esc-XmlText "$($tpl.name)")</name>"
X " <condition>$(Esc-Xml "$($tpl.condition)")</condition>" X "`t`t<condition>$(Esc-XmlText "$($tpl.condition)")</condition>"
X ' </restrictionTemplate>' X "`t</restrictionTemplate>"
$templateCount++ $templateCount++
} }
} }
@@ -799,8 +810,8 @@ if (-not (Test-Path $extDir)) {
} }
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($metadataPath, $metadataXml, $enc) [System.IO.File]::WriteAllText($metadataPath, $metadataXml.TrimEnd("`r", "`n"), $enc)
[System.IO.File]::WriteAllText($rightsPath, $rightsXml, $enc) [System.IO.File]::WriteAllText($rightsPath, $rightsXml.TrimEnd("`r", "`n"), $enc)
# --- 12. Register in Configuration.xml --- # --- 12. Register in Configuration.xml ---
@@ -856,11 +867,26 @@ if (Test-Path $configXmlPath) {
$cfgSettings = New-Object System.Xml.XmlWriterSettings $cfgSettings = New-Object System.Xml.XmlWriterSettings
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true) $cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
$cfgSettings.Indent = $false $cfgSettings.Indent = $false
$stream = New-Object System.IO.FileStream($configXmlPath, [System.IO.FileMode]::Create) $cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
$writer = [System.Xml.XmlWriter]::Create($stream, $cfgSettings) # Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
$configDoc.Save($writer) $configDoc.Save($writer)
$writer.Close() $writer.Flush(); $writer.Close()
$stream.Close()
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$cfgText = [regex]::Replace($cfgText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $configXmlPath) -and ([System.IO.File]::ReadAllText($configXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($configXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
$regResult = "added" $regResult = "added"
} }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# role-compile v1.10 — Compile 1C role from JSON # role-compile v1.24 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import json import json
@@ -10,6 +10,65 @@ import uuid
from lxml import etree from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================ # ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
@@ -189,6 +248,16 @@ def assert_edit_allowed(target_path, require):
def detect_format_version(d): def detect_format_version(d):
while d: while d:
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
ext_path = d + ".xml"
if os.path.isfile(ext_path):
with open(ext_path, "r", encoding="utf-8-sig") as f:
ext_head = f.read(2000)
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
if m:
return m.group(1)
cfg_path = os.path.join(d, "Configuration.xml") cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path): if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f: with open(cfg_path, "r", encoding="utf-8-sig") as f:
@@ -203,7 +272,26 @@ def detect_format_version(d):
return "2.17" return "2.17"
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def detect_eol(text):
# Перевод строки ВСТАВКИ берём из самого файла: канон CRLF относится к файлам,
# которые мы создаём, а правка существующего сохраняет его стиль (#44/#46/#47).
# Семантика та же, что у _detect_xml_style в остальных портах: есть CRLF → CRLF.
# Мажоритарное правило здесь было расхождением — на смешанном входе оно давало
# другой ответ, чем канон, при том же назначении.
return '\r\n' if '\r\n' in text else '\n'
def esc_xml(s): def esc_xml(s):
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def esc_xml_text(s):
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует """Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
(92142 сырых кавычки на корпус, ни одной &quot;); &quot; она принимает, но нормализует обратно.""" (92142 сырых кавычки на корпус, ни одной &quot;); &quot; она принимает, но нормализует обратно."""
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;') return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
@@ -216,7 +304,7 @@ def emit_mltext(lines, indent, tag, text):
lines.append(f"{indent}<{tag}>") lines.append(f"{indent}<{tag}>")
lines.append(f"{indent}\t<v8:item>") lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>") lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>") lines.append(f"{indent}\t\t<v8:content>{esc_xml_text(text)}</v8:content>")
lines.append(f"{indent}\t</v8:item>") lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</{tag}>") lines.append(f"{indent}</{tag}>")
@@ -226,10 +314,13 @@ def new_uuid():
def write_utf8_bom(path, content): def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
# --- Russian synonyms -> canonical English names --- # --- Russian synonyms -> canonical English names ---
TYPE_ALIASES = { TYPE_ALIASES = {
@@ -239,15 +330,18 @@ TYPE_ALIASES = {
"РегистрНакопления": "AccumulationRegister", "РегистрНакопления": "AccumulationRegister",
"РегистрБухгалтерии": "AccountingRegister", "РегистрБухгалтерии": "AccountingRegister",
"РегистрРасчета": "CalculationRegister", "РегистрРасчета": "CalculationRegister",
"РегистрРасчёта": "CalculationRegister",
"Константа": "Constant", "Константа": "Constant",
"ПланСчетов": "ChartOfAccounts", "ПланСчетов": "ChartOfAccounts",
"ПланВидовХарактеристик": "ChartOfCharacteristicTypes", "ПланВидовХарактеристик": "ChartOfCharacteristicTypes",
"ПланВидовРасчета": "ChartOfCalculationTypes", "ПланВидовРасчета": "ChartOfCalculationTypes",
"ПланВидовРасчёта": "ChartOfCalculationTypes",
"ПланОбмена": "ExchangePlan", "ПланОбмена": "ExchangePlan",
"БизнесПроцесс": "BusinessProcess", "БизнесПроцесс": "BusinessProcess",
"Задача": "Task", "Задача": "Task",
"Обработка": "DataProcessor", "Обработка": "DataProcessor",
"Отчет": "Report", "Отчет": "Report",
"Отчёт": "Report",
"ОбщаяФорма": "CommonForm", "ОбщаяФорма": "CommonForm",
"ОбщаяКоманда": "CommonCommand", "ОбщаяКоманда": "CommonCommand",
"Подсистема": "Subsystem", "Подсистема": "Subsystem",
@@ -632,7 +726,7 @@ def main():
parser = argparse.ArgumentParser(description='Compile 1C role from JSON', allow_abbrev=False) parser = argparse.ArgumentParser(description='Compile 1C role from JSON', allow_abbrev=False)
parser.add_argument('-JsonPath', type=str, required=True) parser.add_argument('-JsonPath', type=str, required=True)
parser.add_argument('-OutputDir', type=str, required=True) parser.add_argument('-OutputDir', type=str, required=True)
args = parser.parse_args() args = ci_parse_args(parser)
# --- 1. Load and validate JSON --- # --- 1. Load and validate JSON ---
json_path = args.JsonPath json_path = args.JsonPath
@@ -641,7 +735,7 @@ def main():
sys.exit(1) sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f: with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.load(f) defn = ci_json(json.load(f))
if not defn.get('name'): if not defn.get('name'):
print("JSON must have 'name' field (role programmatic name)", file=sys.stderr) print("JSON must have 'name' field (role programmatic name)", file=sys.stderr)
@@ -673,90 +767,80 @@ def main():
# --- 4. Emit metadata XML (Roles/Name.xml) --- # --- 4. Emit metadata XML (Roles/Name.xml) ---
lines = [] lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>') lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"') # Объявления пространств имён — одной переменной и одной строкой, как пишет платформа.
lines.append(' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"') xmlns_decl = ('xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
lines.append(' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"') # 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
lines.append(' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"') # style): платформа держит объявления по алфавиту. В Rights.xml палитра НЕ идёт.
lines.append(' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"') if format_rank(format_version) >= 221:
lines.append(' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"') xmlns_decl = xmlns_decl.replace(
lines.append(' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"') ' xmlns:style=',
lines.append(' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"') ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
lines.append(' xmlns:v8="http://v8.1c.ru/8.1/data/core"') lines.append(f'<MetaDataObject {xmlns_decl} version="{format_version}">')
lines.append(' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"') lines.append(f'\t<Role uuid="{uid}">')
lines.append(' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"') lines.append('\t\t<Properties>')
lines.append(' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"') lines.append(f'\t\t\t<Name>{role_name}</Name>')
lines.append(' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"') lines.append('\t\t\t<Synonym>')
lines.append(' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"') lines.append('\t\t\t\t<v8:item>')
lines.append(' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"') lines.append('\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(' xmlns:xs="http://www.w3.org/2001/XMLSchema"') lines.append(f'\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>')
lines.append(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"') lines.append('\t\t\t\t</v8:item>')
lines.append(f' version="{format_version}">') lines.append('\t\t\t</Synonym>')
lines.append(f' <Role uuid="{uid}">')
lines.append(' <Properties>')
lines.append(f' <Name>{role_name}</Name>')
lines.append(' <Synonym>')
lines.append(' <v8:item>')
lines.append(' <v8:lang>ru</v8:lang>')
lines.append(f' <v8:content>{esc_xml(synonym)}</v8:content>')
lines.append(' </v8:item>')
lines.append(' </Synonym>')
if comment: if comment:
lines.append(f' <Comment>{esc_xml(comment)}</Comment>') lines.append(f'\t\t\t<Comment>{esc_xml_text(comment)}</Comment>')
else: else:
lines.append(' <Comment/>') lines.append('\t\t\t<Comment/>')
lines.append(' </Properties>') lines.append('\t\t</Properties>')
lines.append(' </Role>') lines.append('\t</Role>')
lines.append('</MetaDataObject>') lines.append('</MetaDataObject>')
metadata_xml = '\n'.join(lines) + '\n' metadata_xml = '\r\n'.join(lines)
# --- 5. Emit Rights XML (Roles/Name/Ext/Rights.xml) --- # --- 5. Emit Rights XML (Roles/Name/Ext/Rights.xml) ---
lines = [] lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>') lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<Rights xmlns="http://v8.1c.ru/8.2/roles"') lines.append('<Rights xmlns="http://v8.1c.ru/8.2/roles" xmlns:xs="http://www.w3.org/2001/XMLSchema"'
lines.append(' xmlns:xs="http://www.w3.org/2001/XMLSchema"') ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Rights"'
lines.append(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"') f' version="{format_version}">')
lines.append(f' xsi:type="Rights" version="{format_version}">')
# Global flags # Global flags
sfno = str(defn['setForNewObjects']).lower() if defn.get('setForNewObjects') is not None else 'false' sfno = str(defn['setForNewObjects']).lower() if defn.get('setForNewObjects') is not None else 'false'
sfab = str(defn['setForAttributesByDefault']).lower() if defn.get('setForAttributesByDefault') is not None else 'true' sfab = str(defn['setForAttributesByDefault']).lower() if defn.get('setForAttributesByDefault') is not None else 'true'
irco = str(defn['independentRightsOfChildObjects']).lower() if defn.get('independentRightsOfChildObjects') is not None else 'false' irco = str(defn['independentRightsOfChildObjects']).lower() if defn.get('independentRightsOfChildObjects') is not None else 'false'
lines.append(f' <setForNewObjects>{sfno}</setForNewObjects>') lines.append(f'\t<setForNewObjects>{sfno}</setForNewObjects>')
lines.append(f' <setForAttributesByDefault>{sfab}</setForAttributesByDefault>') lines.append(f'\t<setForAttributesByDefault>{sfab}</setForAttributesByDefault>')
lines.append(f' <independentRightsOfChildObjects>{irco}</independentRightsOfChildObjects>') lines.append(f'\t<independentRightsOfChildObjects>{irco}</independentRightsOfChildObjects>')
# Object blocks # Object blocks
total_rights = 0 total_rights = 0
for obj in parsed_objects: for obj in parsed_objects:
lines.append(' <object>') lines.append('\t<object>')
lines.append(f' <name>{obj["Name"]}</name>') lines.append(f'\t\t<name>{obj["Name"]}</name>')
for right in obj['Rights']: for right in obj['Rights']:
lines.append(' <right>') lines.append('\t\t<right>')
lines.append(f' <name>{right["Name"]}</name>') lines.append(f'\t\t\t<name>{right["Name"]}</name>')
lines.append(f' <value>{right["Value"]}</value>') lines.append(f'\t\t\t<value>{right["Value"]}</value>')
if right['Condition']: if right['Condition']:
lines.append(' <restrictionByCondition>') lines.append('\t\t\t<restrictionByCondition>')
lines.append(f' <condition>{esc_xml(right["Condition"])}</condition>') lines.append(f'\t\t\t\t<condition>{esc_xml_text(right["Condition"])}</condition>')
lines.append(' </restrictionByCondition>') lines.append('\t\t\t</restrictionByCondition>')
lines.append(' </right>') lines.append('\t\t</right>')
total_rights += 1 total_rights += 1
lines.append(' </object>') lines.append('\t</object>')
# RLS restriction templates # RLS restriction templates
template_count = 0 template_count = 0
if defn.get('templates'): if defn.get('templates'):
for tpl in defn['templates']: for tpl in defn['templates']:
lines.append(' <restrictionTemplate>') lines.append('\t<restrictionTemplate>')
lines.append(f' <name>{esc_xml(str(tpl["name"]))}</name>') lines.append(f'\t\t<name>{esc_xml_text(str(tpl["name"]))}</name>')
lines.append(f' <condition>{esc_xml(str(tpl["condition"]))}</condition>') lines.append(f'\t\t<condition>{esc_xml_text(str(tpl["condition"]))}</condition>')
lines.append(' </restrictionTemplate>') lines.append('\t</restrictionTemplate>')
template_count += 1 template_count += 1
lines.append('</Rights>') lines.append('</Rights>')
rights_xml = '\n'.join(lines) + '\n' rights_xml = '\r\n'.join(lines)
# --- 6. Write output files --- # --- 6. Write output files ---
out_dir = args.OutputDir out_dir = args.OutputDir
@@ -791,9 +875,13 @@ def main():
reg_result = None reg_result = None
if os.path.exists(config_xml_path): if os.path.exists(config_xml_path):
with open(config_xml_path, 'r', encoding='utf-8-sig') as f: # newline='' => без трансляции переводов строк: иначе CRLF молча схлопнется
# в LF при чтении и файл будет переписан в LF.
with open(config_xml_path, 'r', encoding='utf-8-sig', newline='') as f:
raw_text = f.read() raw_text = f.read()
eol = detect_eol(raw_text)
# Check if already registered # Check if already registered
if f'<Role>{role_name}</Role>' in raw_text: if f'<Role>{role_name}</Role>' in raw_text:
reg_result = 'already' reg_result = 'already'
@@ -807,10 +895,15 @@ def main():
# Insert after last existing <Role> # Insert after last existing <Role>
last_match = matches[-1] last_match = matches[-1]
insert_pos = last_match.end() insert_pos = last_match.end()
raw_text = raw_text[:insert_pos] + f'\n\t\t\t{new_role_tag}' + raw_text[insert_pos:] raw_text = raw_text[:insert_pos] + eol + f'\t\t\t{new_role_tag}' + raw_text[insert_pos:]
else: else:
# No existing roles — insert before </ChildObjects> # No existing roles — insert before </ChildObjects>
raw_text = raw_text.replace('</ChildObjects>', f'\t\t\t{new_role_tag}\n\t\t</ChildObjects>') # Отступ вставки берём у закрывающего тега +1 уровень: подстановка
# по голому '</ChildObjects>' удваивала бы уже присутствующий отступ
# строки (получалось 5 табов вместо 3 — PS-порт через DOM даёт 3).
raw_text = re.sub(r'([ \t]*)</ChildObjects>',
lambda m: m.group(1) + '\t' + new_role_tag + eol + m.group(1) + '</ChildObjects>',
raw_text, count=1)
write_utf8_bom(config_xml_path, raw_text) write_utf8_bom(config_xml_path, raw_text)
reg_result = 'added' reg_result = 'added'
@@ -1,4 +1,4 @@
# role-info v1.2 — Analyze 1C role rights # role-info v1.4 — Analyze 1C role rights (+единое имя хелпера состояния поддержки)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$RightsPath, [Parameter(Mandatory=$true)][Alias('Path')][string]$RightsPath,
+27 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# role-info v1.2 — Analyze 1C role rights # role-info v1.4 — Analyze 1C role rights (+единое имя хелпера состояния поддержки)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -12,6 +12,28 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Argument parsing --- # --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C role rights", allow_abbrev=False) parser = argparse.ArgumentParser(description="Analyze 1C role rights", allow_abbrev=False)
parser.add_argument("-RightsPath", "-Path", required=True, help="Path to Rights.xml") parser.add_argument("-RightsPath", "-Path", required=True, help="Path to Rights.xml")
@@ -19,7 +41,7 @@ parser.add_argument("-ShowDenied", action="store_true", default=False, help="Sho
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show") 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") parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file") parser.add_argument("-OutFile", default="", help="Write output to file")
args = parser.parse_args() args = ci_parse_args(parser)
# --- Output helper (collect all, paginate at the end) --- # --- Output helper (collect all, paginate at the end) ---
lines_buf = [] lines_buf = []
@@ -161,7 +183,7 @@ def get_support_status_for_path(target_path):
except Exception: except Exception:
pass pass
return None return None
def is_external_root(xml_path): def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path): if not os.path.isfile(xml_path):
return False return False
try: try:
@@ -175,14 +197,14 @@ def get_support_status_for_path(target_path):
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
elem_uuid = root_uuid(rp) elem_uuid = root_uuid(rp)
if is_external_root(rp): if _sg_is_external_root(rp):
return None return None
bin_path = None bin_path = None
d = os.path.dirname(rp) d = os.path.dirname(rp)
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if is_external_root(d + ".xml"): if _sg_is_external_root(d + ".xml"):
return None return None
if not elem_uuid: if not elem_uuid:
elem_uuid = root_uuid(d + ".xml") elem_uuid = root_uuid(d + ".xml")
@@ -1,4 +1,4 @@
# role-validate v1.1 — Validate 1C role structure # role-validate v1.2 — Validate 1C role structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]

Some files were not shown because too many files have changed in this diff Show More