Compare commits

...
165 Commits
Author SHA1 Message Date
Nick ShirokovandClaude Opus 5 83bb5b6fd3 fix(cfe-borrow): свойства и виды, от которых зависят стандартные поля
Сплошной прогон по типам объектов (11 минимальных объектных форм УТ, каждая
заимствована и загружена в UT_DEMO) дал три падения из одиннадцати. Все три —
один класс: в оболочке не хватает того, от чего зависит существование
стандартного поля, и платформа отвергает загрузку «Неверный путь к данным».

1. Справочник с владельцем: «Объект.Owner» не разрешается без <Owners>.
   Свойство — список <xr:Item>, а не скаляр, поэтому переносится фрагментом,
   как __TypeXml у DefinedType. Одного переноса мало: ссылка должна вести на
   объект, который в расширении есть, иначе платформа падает с access violation
   вместо сообщения. Добавлен общий проход, заимствующий владельцев (и владельцев
   владельцев) — Конфигуратор поступает так же, эталон Issue66Example7_1.

2. Регистр сведений: «Запись.Period» не разрешается без
   InformationRegisterPeriodicity. Вместе с ним переносится WriteMode, от
   которого зависит «Запись.Recorder» — Конфигуратор несёт оба, 6 эталонов из 6.

3. Задача: «Объект.Исполнитель» — это реквизит адресации, отдельный вид
   дочернего объекта. AddressingAttribute добавлен к видам, которые
   заимствуются поимённо, рядом с Dimension и Resource.

Правка сделана в Read-SourceObject и Build-BorrowedObjectXml — там, где оболочка
рождается: механизм $extraProps работает только в потоке -BorrowMainAttribute и
обычное заимствование оболочки не покрывает.

Проверено: те же 11 форм грузятся 11 из 11; ПВХ с путём «Объект.ValueType»
грузился и раньше — Конфигуратор Type и CharacteristicExtValues тоже не
переносит (эталон Issue66Example8), правило «свойство, включающее поле»
подтверждается с обеих сторон.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 19:43:23 +03:00
Nick ShirokovandClaude Opus 5 112166dc0c fix(cfe-borrow): стандартные поля и Items-пути в связях параметров выбора
Эталоны Issue66Example7 и 7_1 (три формы, оба режима) показали, что прошлая
правка была неполной в двух местах.

1. Путь на СТАНДАРТНОЕ поле объекта («Объект.Owner», «Объект.Date», «Объект.Ref»)
текстом не разрешается даже при заимствованном основном реквизите: платформа
отвергает загрузку «Неверный путь к данным». Мы такой путь оставляли текстом.
Конфигуратор оставляет ссылку на сам реквизит — «1». Теперь так же: реквизит
объекта (есть в ChildObjects) остаётся читаемым текстом, стандартное поле
сводится к id основного реквизита. Проверено загрузкой: было падение, стало
успешно; выход совпал с эталоном 7_1 дословно.

2. Пути «Items.<Элемент>.CurrentData.<Поле>» при заимствованном основном
реквизите разрешаются текстом — элементы формы на месте, а их данные доступны
через основной реквизит. Конфигуратор их и не трогает. Прошлая правка вырезала
их в обоих режимах, то есть теряла рабочие связи. Теперь вырезаются только без
заимствования основного реквизита, где они действительно не разрешаются.

Уточнение по кодировке для будущей работы: без заимствования Конфигуратор
кодирует стандартное поле как «1/-N» (Owner у справочника и Ref у документа
дали -5, Date у документа -3), а Items-путь — как
«<id элемента>:02023637-7868-4a5f-8576-835a76e0c9ba/0:<uuid поля>». Таблицу
отрицательных индексов по трём точкам не построить, поэтому в скелетном режиме
обе разновидности по-прежнему вырезаются с предупреждением.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 18:42:20 +03:00
Nick ShirokovandClaude Opus 5 ace9e29e91 test(cfe-borrow): платформенная верификация кейсов на связи параметров выбора
Прогон verify-snapshots вскрыл, что два новых кейса опирались на вход, который
платформа не принимает — падала конфигурация-источник, а не расширение.

form-choice-link-items: путь «Items.Ячейка.CurrentData.Склад» был невалиден,
потому что Ячейка — поле ввода, а не таблица. Кейс пересобран на настоящей
табличной части: путь корректен, связь по-прежнему вырезается, фикстура
грузится. Кейс продолжает падать на коде до правки.

form-choice-link-dangling: вход намеренно содержит путь на несуществующий
реквизит объекта — в валидной конфигурации такой формы не бывает, а проверяется
именно поведение навыка на таком пути. Помечен skipPlatformVerify с причиной.

После правок verify-snapshots --skill cfe-borrow: 21 прошло, 0 упало,
1 пропущен по объявленной причине. cfe-validate — 7 из 7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 17:46:55 +03:00
Nick ShirokovandClaude Opus 5 6c491c4f2e fix(cfe-borrow): ссылки параметров выбора на реквизит формы — по id базовой формы
Третий и последний класс дефектов из ишьи #66, единственный, который валит
загрузку. Rewrite-ChoiceParameterLinks переписывал только пути от «Объект.»;
ссылка с корнем-реквизитом ФОРМЫ проходила текстом, а реквизиты формы не
заимствуются никогда — путь не разрешался:

  Неверный путь к полю - Помещение
  Неверный путь к полю - Склад
  Неверный путь к полю - ОтборСклад

Связь с постскриптумом репортёра прямая: в эталоне JR2976 ссылка
«ИспользоватьСоглашенияСКлиентами» — то самое имя из его сообщения —
закодирована как «51». В корпусе УТ таких форм 103, путей 424 из 1015.

Правило подтверждено на шести расширениях от Конфигуратора (Issue66Example4/5/6,
JR2433, JR2976, JR49904): подставляется id реквизита ИСХОДНОЙ формы. Именно
исходной — Issue66Example6 показывает, что даже с заимствованными реквизитами
формы (id 1000002/1000003) ссылки продолжают указывать на 4 и 5, то есть в
нумерацию базовой формы. Переписывать нужно в обоих режимах: реквизиты формы не
заимствуются ни при Form, ни при All, поэтому гейт «только без основного
реквизита» снят.

Путь «Объект.X» при заимствованном основном реквизите оставлен текстом:
он разрешается (проверено загрузкой и UpdateDBCfg) и читается. Конфигуратор
нормализует и его, но копировать обфускацию там, где она не нужна, незачем.
Зашитая единица в «1/0:<uuid>» заменена на реальный id основного реквизита
исходной формы.

Путь, который не сводится ни к одному известному виду, теперь вырезается с
предупреждением, а не остаётся текстом. Сюда попадают
«Items.<Элемент>.CurrentData.<Поле>» (303 в УТ): их кодировка непрозрачна и по
имеющимся эталонам не воспроизводима. Проверено — с таким путём форма не
грузилась вовсе; без связи заимствуется и работает. Связь параметров выбора —
удобство подбора, а не данные.

Проверено на UT_DEMO: расширение с обеими формами грузится и применяется к БД,
значения ссылок совпадают с эталонами (4, 5, 2) в обоих режимах и обоих портах.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 16:49:07 +03:00
Nick ShirokovandClaude Opus 5 12d085aa8d fix(cfe-validate): не проверять пути формы списка по составу объекта
Расшивание корня путей (8366dd49) включило проверку 14 на формах списка, и она
дала ложные ошибки на реальных формах: 'Список.DefaultPicture' и
'Список.ПредставлениеСостояния' объявлены висячими, хотя форма валидна.

У динамического списка набор полей — результат его запроса, а не состав
объекта: туда входят и стандартные поля списка, и псевдонимы запроса. Сверять
такие пути с ChildObjects нельзя в принципе. Корпусная проверка: на 1094 формах
списка УТ таких сегментов 3383 — DefaultPicture (1009), Ref (551), Date (341),
Number (331), Description (311) и псевдонимы вроде ПредставлениеСостояния.

Проверка 14 теперь пропускает формы, у которых основной реквизит —
динамический список. Проверка 12 и тексты сообщений параметризацию сохраняют:
там корень берётся из атрибута table=, и он совпадает с основным реквизитом в
339 случаях из 389, а в остальных не совпадал и до правки.

Кейс на висячий путь формы списка снят — его посылка была неверной; вместо него
кейс на отсутствие ложных ошибок, он падает при снятии гарда.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 15:55:29 +03:00
Nick ShirokovandClaude Opus 5 8366dd49de fix(cfe-validate): корень путей из основного реквизита формы
Проверки 12 (<AdditionalColumns table="...">) и 14 (пути против конфигурации-
источника) искали пути регуляркой, зашитой на корень «Объект». Он такой только
у формы объекта: у формы списка «Список», у формы записи регистра «Запись».
На таких формах регулярка не совпадала, и обе проверки молча не срабатывали —
валидатор рапортовал «чисто» на форме с висячим путём.

Корень теперь берётся из основного реквизита: сначала из <Attributes> самой
формы, затем из <BaseForm>. Нет его нигде — путей с корнем не бывает, проверка
пропускается. Имя подставляется и в регулярки, и в тексты сообщений: раньше в
сообщении стояло «Объект.X» независимо от формы, что дезинформировало.

Вместе с этим набор имён-кандидатов проверки 14 расширен с Attribute/
TabularSection до Dimension/Resource. Без этого замена корня превратила бы
тихий пропуск в ложные ошибки на форме записи регистра, где дочерние объекты —
измерения и ресурсы; отдельный кейс это фиксирует.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 15:42:41 +03:00
Nick ShirokovandClaude Opus 5 c63b626dd4 fix(form-validate): корень висячих путей — из основного реквизита формы
Проверка 11d искала висячие пути регуляркой, зашитой на корень «Объект».
Он такой только у формы объекта: у формы списка корень «Список», у формы
записи регистра «Запись». На таких формах регулярка не совпадала, и проверка
молча не срабатывала — валидатор рапортовал «чисто» на форме, которую
платформа отвергает с «Неверный путь к полю».

Узел основного реквизита BaseForm проверка уже доставала строкой выше, но
использовала только как булев признак. Теперь из него берётся имя — и для
регулярки, и для текста сообщения. Если основного реквизита нет и в BaseForm,
корень неизвестен и поведение остаётся прежним.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 15:28:53 +03:00
Nick ShirokovandClaude Opus 5 a424e9cd19 fix(cfe-borrow): текст запроса динамического списка — тоже место ссылки
Заимствование дочерних объектов в оболочку шло по трём источникам имён:
*DataPath, <Field> и <AdditionalColumns table=...>. У формы списка с ручным
запросом реквизиты упомянуты ещё и в тексте запроса, и они не заимствовались:
на эталоне Issue66Example2 навык брал 5 реквизитов и 0 табличных частей,
Конфигуратор — 17 и 4.

Правило выведено по эталонам и совпадает с ними в обе стороны: Конфигуратор
переносит то, на что ссылается форма, а у динамического списка с ручным
запросом текст запроса — такое же место ссылки, как DataPath. Имена, которые
встречаются в <QueryText> целым словом, — это 21 из 27 дочерних объектов, и
множество совпадает со взятым Конфигуратором точно.

Разбирать язык запросов не нужно: имена-кандидаты и так фильтруются по
реальному составу объекта, поэтому лишние слова из запроса безвредны.

Состав оболочки теперь совпадает с эталонами на всех четырёх случаях:
форма документа 47 реквизитов и 2 ТЧ, список с запросом 17 и 4, список без
запроса 3 и 0 (Issue66Example3 — там правило «только видимое на форме», и
поведение не меняется), форма записи регистра 3 измерения и 1 ресурс.

По корпусу (13796 форм УТ + ERP) списков с запросом 2584, без него 2411.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 15:25:31 +03:00
Nick ShirokovandClaude Opus 5 67d7da8285 test(cfe-borrow): кейсы на необъектный основной реквизит и повторное заимствование
Кейсов на эти сценарии не было ни одного, поэтому оба дефекта жили молча.

Фикстура main-attr-kinds: документ с табличной частью и тремя формами
(документа, списка с динамическим списком и <Settings>) плюс регистр
сведений с измерением и ресурсом и его форма записи.

Три кейса:
- форма списка с -BorrowMainAttribute: реквизит «Список» типа DynamicList
  переносится вместе с <Settings>, пути «Список.*» не вырезаны, «Объект» и
  придуманного <SavedData> в выходе нет;
- форма записи регистра: реквизит «Запись» нужного типа, а в оболочке
  <Dimension>/<Resource>, а не <Attribute>;
- повторное заимствование по объекту с табличной частью: второй заход
  добавляет недостающий реквизит, ТЧ не вложены друг в друга.

Все три падают на коде до правок и проходят после.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 14:45:40 +03:00
Nick ShirokovandClaude Opus 5 d3ea826286 fix(cfe-borrow): основной реквизит формы переносится из источника
Реквизит синтезировался с зашитыми name="Объект", типом <Тип>Object и
SavedData=true, а сверху доклеивались «довески» исходной формы. Для формы
списка это давало реквизит «Объект» объектного типа с настройками
динамического списка внутри, и платформа отвергала файл: «Исключение XDTO
произошло при чтении файла». Навык при этом отрабатывал молча, exit 0.

По корпусу (8675 форм с основным реквизитом, УТ + ERP) объектных только 4074:
динамический список — 3847, менеджер записи — 546, xs:string — 108, набор
констант — 97. <Settings xsi:type="DynamicList"> встречается ровно у
динамических списков, 3847 из 3847.

Эталоны Конфигуратора дают единое правило для всех видов: реквизит копируется
из исходной формы дословно, меняется только id. Теперь так и делается, а
вычисление типа из таблицы GeneratedType уходит вместе с тихим сбоем
«нет категории Object → cfg:.Имя» для перечислений, констант и регистров.

Следом расшиты ещё три места, зашитые на литерал «Объект»:
- вырезание путей к данным — корень берётся из имени основного реквизита,
  иначе у формы списка вырезало бы все пути «Список.*»; путь ровно на сам
  реквизит («Список» у таблицы формы) тоже сохраняется;
- сбор путей для заимствования дочерних объектов — на необъектных формах не
  находил ничего, и в оболочку не попадало ни одного реквизита;
- вид дочернего объекта: измерения и ресурсы регистра переносятся как
  <Dimension>/<Resource>, а не все как <Attribute>.

Проверено на трёх эталонах (форма документа, форма списка, форма записи
регистра): основной реквизит, корни путей к данным и виды дочерних объектов
оболочки совпадают. Расширение с формой списка и формой записи грузится в
UT_DEMO и применяется к БД — до правки загрузка падала.

Снапшоты: <Type> у основного реквизита теперь многострочный, как у
Конфигуратора; синтез писал его в одну строку.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 14:32:54 +03:00
Nick ShirokovandClaude Opus 5 b5662a380d fix(cfe-borrow): вставка только в собственный ChildObjects объекта
Заимствованное содержимое вставлялось текстом по ВСЕМ вхождениям
<ChildObjects>, а у объекта с табличными частями свой контейнер один, но
вхождений столько же, сколько ТЧ. При повторном заимствовании по тому же
объекту ps1 выдавал невалидный XML («mismatched tag»), py — валидный, но с
табличными частями внутри табличной части и дублем ТЧ.

Отдельно ветка «Replace empty ChildObjects» в ps1 переписывала КАЖДЫЙ блок
содержимым первого — теряла данные.

Свой <ChildObjects> закрывается в файле последним: объект в файле один,
вложенные ТЧ закрываются раньше. Правило вынесено в общую функцию каждого
порта, на неё переведены все места вставки.

Дедуп при повторном заимствовании брал имена регуляркой «первый
<ChildObjects> до первого </ChildObjects>» — у объекта с ТЧ этот отрезок
обрывается на закрытии первой ТЧ: захватывает имена её колонок и теряет всё,
что идёт после. Заменён разбором прямых детей своего контейнера. В py дедуп
дополнительно сканировал все <Name> во всём файле, включая имя самого объекта.

Проверено: два заимствования подряд по одному документу дают в обоих портах
валидный XML, 5 табличных частей на верхнем уровне, вложенности и дублей нет;
выходы портов отличаются только свежими uuid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 14:13:20 +03:00
Nick ShirokovandClaude Opus 5 8c915df0d9 fix(cfe-borrow): паритет портов при дозаписи реквизитов в оболочку
PS вставлял лишнюю строку с табуляцией перед первым <Attribute>, когда
дозаписывал реквизиты в заимствованную оболочку с самозакрытым
<ChildObjects/>: элемент раскрывался пробельным узлом в DOM, а последующая
текстовая вставка добавляла отступ ещё раз. У Конфигуратора в таких файлах
пустых строк нет ни одной, py-порт делал правильно.

PS сведён к тому же текстовому раскрытию, что и py. Расхождение портов на
Document.ВнутреннееПотребление (-BorrowMainAttribute Form) закрыто: выходы
отличаются только свежесгенерированными uuid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:31:20 +03:00
Nick ShirokovandClaude Opus 5 656160407a test(cfe-borrow): кейсы на свойства формы после CommandSet
В фикстурах cfe-borrow CommandSet не встречался ни разу, поэтому потеря
свойств не ловилась. Добавлена фикстура doc-form-commandset — форма документа
с реальным порядком секций (CommandSet, затем AutoTime/UsePostingMode/
RepostOnWrite), вложенным CommandSet у таблицы, RowPictureDataPath от Объект.
и CommandInterface. Форма собрана файлом, а не form-compile: тот выдаёт
свойства ДО CommandSet, то есть как раз не тот порядок, который нужен.

Два кейса над ней — заимствование без основного реквизита и с ним. Оба падают
на коде до правки и проходят после.

README: задокументированы ключи expect fileContains/fileNotContains/filesEqual,
которые раннер понимает, но в таблице их не было. Плюс предупреждение, что
подстрока сверяется буквально: ожидание "<CommandSet></CommandSet>" не
срабатывает на переводе строки внутри и потому не проверяет ничего.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:11:36 +03:00
Nick ShirokovandClaude Opus 5 0737a79522 fix(cfe-borrow): секции формы отбираются по имени, а не по позиции
Свойства уровня формы собирались до первой «визуальной» секции. В этот
список входил CommandSet, который в реальных формах стоит В СЕРЕДИНЕ блока
свойств: у всех 794 форм документов ERP с CommandSet он предшествует
AutoCommandBar, а AutoTime/UsePostingMode/RepostOnWrite идут после него.
Отсечка теряла весь хвост — в одном ERP это 2062 формы.

Отказа загрузки это не давало: платформа принимает форму и молча подставляет
дефолты. На Document.ЧекККМ.ФормаДокумента (УТ) AutoTime DontUse становился
CurrentOrLast, UsePostingMode Regular — Auto; исключённые стандартные команды
(Провести, Записать, Отменить проведение) возвращались в панель.

Граница теперь — AutoCommandBar: он есть в 100% из 30223 форм корпуса, и ни
одно свойство никогда не стоит после него. Отбрасываются явным списком
Events/Attributes/Commands/Parameters/CommandInterface и свойства, значение
которых — имя реквизита формы (ReportResult, DetailsData, VariantAppearance,
GroupList): реквизиты не заимствуются, ссылка повисла бы.

Заодно разобран конфаунд коммита 7abe26af, менявшего три вещи разом с одной
атрибуцией ошибки. По эталонам Конфигуратора («Добавить в расширение»):
- корневой CommandSet переносится дословно — вырезание было ошибкой;
- вложенный CommandSet выбрасывается ЦЕЛИКОМ, а не опустошается до пустого
  контейнера, как делалось;
- RowPictureDataPath — путь к данным, а не индекс картинки: сохраняется при
  заимствовании основного реквизита (Объект.Товары.РасхождениеЗаказ) и
  вырезается без него (Список.DefaultPicture). Переехал в formBindingDataTags.

Спецификация форм утверждала, что свойства идут до CommandSet/AutoCommandBar —
именно это заблуждение и было закодировано. Заменено фактическим порядком
секций, выведенным из корпуса (0 нарушений на 30223 формах). Там же поправлены
значения перечислений AutoTime/UsePostingMode/ReportFormType, которых в корпусе
нет ни одного.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:10:53 +03:00
Nick ShirokovandClaude Opus 5 78fc3ba71a test(fixtures): режим совместимости roundtrip-фикстур согласован с версией формата
Фикстуры roundtrip-кейсов объявляли формат 2.17 (это 8.3.24) и при этом режим
совместимости Version8_3_27. Из-за этого verify-snapshots на автодетекте платформы
(8.3.24) не мог загрузить их вообще: «Для работы с конфигурацией необходима версия
платформы не меньше, чем 8.3.27». Падение выглядело регрессией навыка, а было
свойством фикстуры, и платформенная верификация девяти кейсов молча не работала.

Режим приведён к формату (Version8_3_24) во всех девяти фикстурах — они дублируются
по навыкам и обязаны оставаться копиями. Эталоны пересняты точечно: дифф —
ровно две строки на файл.

verify-snapshots без --v8path: role-compile 13/0, cf-edit 14/0, meta-edit 20/0,
meta-compile 79/0, subsystem-edit 9/0, xdto-compile 12/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 21:05:53 +03:00
Nick ShirokovandClaude Opus 5 877fde8dd4 test(role-compile,role-validate): платформенная верификация снапшотов и учёт пропусков
Прогон verify-snapshots вскрыл, что три новых кейса не доезжают до платформы:
роль ссылается на объекты, которых нет в фикстуре и которые не создаёт ни один
навык (веб-сервис, внешний источник данных, перерасчёт), а фикстуры валидатора —
это только каталог Roles/ без Configuration.xml. Объявлены skipPlatformVerify
с причиной, чтобы пропуск был видимым, а не молчаливым.

expect.filesAbsent внесён в таблицу ключей README: недокументированный ключ,
который понимает только один раннер, даёт тихую дыру.

Проверено: verify-snapshots на 8.3.27 — role-compile 13/0/1, role-validate 5/0/2;
число пропусков сверено с expected-skips на обеих ОС и обоих портах.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 20:38:00 +03:00
Nick ShirokovandClaude Opus 5 9b0bff1386 fix(role-compile,role-validate): закрытый белый список типов и прав вместо предупреждений
Неизвестный тип объекта обрабатывался веткой «warning» и всё равно попадал в
Rights.xml: и генератор, и валидатор рапортовали успех. Блок прав на тип, который
прав не имеет, платформа не отвергает — конфигурация с правами на перечисление не
загружается в информационную базу, конфигуратор зависает без сообщений.

Белый список — дерево редактора ролей: 27 типов (добавлен ExternalDataSource,
которого не было ни в одной таблице) плюс таблица видов вложенности с наборами прав
и привязкой вида к типу-родителю. Значения сняты с корпуса (acc/erp/ut/unf, ~2750
ролей) и с выгрузок роли со всеми проставленными правами — для внешнего источника
данных и перерасчёта регистра расчёта, которых в корпусе нет.

- role-compile: отказ ДО записи файлов, все причины разом, exit 1; ни файлов роли,
  ни записи в Configuration.xml. Русские алиасы для типов без прав — ради внятного
  отказа, а не ради генерации.
- role-validate: те же случаи — ошибка вместо предупреждения.
- Побочно снято 424 ложных предупреждения на корпусе: права Use у операций
  веб-сервисов и методов HTTP-сервисов считались недопустимыми для вложенных объектов.
- Проверка имён прав и вложенных путей больше не пропускает мусор: неизвестное право
  и путь вида Enum.Х.Attribute.Y раньше не проверялись вовсе.
- docs/1c-role-spec.md: ExternalDataSource ошибочно числился типом без прав.
- dsl-reference.md переписан под «что можно написать» (тип → права, виды
  вложенности) вместо таблиц прощающего ввода.

Тесты: expect.filesAbsent в раннере; новые кейсы на отказ и на вложенные объекты.
Починены кейсы, которые ничего не проверяли: три задавали права одной строкой
"Read View" (навык писал в эталон несуществующее право), valid-role собирал роль
неизвестным ключом и валидировал пустую, bad-root проходил на «файл не найден».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 20:12:15 +03:00
Nick ShirokovandClaude Opus 5 30c8901615 docs(1c-configuration-spec): 8.3.23 → 2.16 замерена, дельта вниз от 2.17 уточнена
Платформа 8.3.23 доустановлена, замер тем же способом (пустая ИБ создаётся и выгружается
одной платформой) дал 2.16 — сообщение из issue #63 подтверждено собственным измерением,
пометка «по issue #63» из таблицы снята.

Незамеренными остались только 8.3.21/8.3.22, и пробел зажат с обеих сторон: 2.13 на 8.3.20
и 2.16 на 8.3.23 — ровно +3 версии на 3 релиза, то есть равномерный шаг подтверждается
арифметически.

Заодно расщепилась дельта пустой конфигурации ниже 2.17, которая раньше была снята одной
точкой: AllowedIncomingShareRequestTypes приезжает ровно в 2.17, DatabaseTablespacesUseMode
и DefaultReportAppearanceTemplate — где-то в 2.14–2.16. Гейт по версии ни одному из них не
нужен: это свойства корня конфигурации (реестр meta-validate — про объекты), а внутри
проверенного диапазона все три существуют всегда.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 18:27:51 +03:00
Nick ShirokovandClaude Opus 5 a1b33cdda0 fix(cf-init): предупреждать про CompatibilityMode DontUse вместо строки в SKILL.md
«Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы: свежая база
получает Version8_3_<своя> (замерено на 8.3.20/8.3.24/8.3.25/8.3.27/8.5.1), и ни одна из
пяти типовых в корпусе DontUse не содержит. Само значение легально — платформа принимает
его без ошибок, — но не выживает: на 8.3.25 и на 8.3.27 выгрузка возвращает Version8_3_8.
Контроль: тот же шаблон cf-init с явным Version8_3_25 роундтрипится без изменений.

Отсюда предупреждение, а не запрет: запрещать значение, которое платформа принимает, —
то же самое, от чего мы только что ушли в -FormatVersion. Канал тот же (stderr, exit 0),
что и у предупреждения о версии формата.

Абзац про DontUse убран из SKILL.md: инструкция читается всегда, а ловушка актуальна почти
никогда, и текст сам ЗНАКОМИЛ модель со значением, которое выбирать не следует.

Сравнение регистронезависимо явно в обоих портах: в PS -eq таков по умолчанию, в py — нет,
и молчаливое расхождение портов началось бы прямо здесь.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 18:05:57 +03:00
Nick ShirokovandClaude Opus 5 48f9bc1d52 fix(tests): check-uuid-invariant — ошибку прогона не называть нарушением инварианта
Счётчик был один: несобранное окружение давало «1 НАРУШЕНИЙ инварианта uuid» и
отправляло искать баг там, где его нет. Счётчики разведены, exit по-прежнему 1
в обоих случаях — непрогнанный порт это не «зелено».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 16:03:26 +03:00
Nick ShirokovandClaude Opus 5 f38fcce692 fix(tests): check-uuid-invariant работает на macOS
Гард запускает навыки в обоих портах и зашивал `powershell.exe` и `python`. Вне Windows
он падал с «spawnSync powershell.exe ENOENT» и красил весь check-all, хотя PowerShell на
маке не исполняется в принципе — это природа платформы, а не пробел в покрытии.

PowerShell отсеивается по ОС с явной строкой о пропуске; интерпретатор python на *nix —
python3. Если запрошен только powershell и ОС не Windows, выходим с 1: молча зеленеть,
ничего не проверив, хуже, чем упасть.

Гард запускает НАВЫКИ, а им нужен интерпретатор с lxml — системный python3 на маке его не
имеет. На ошибке импорта печатается подсказка про PYTHON=<venv>, иначе окружение читается
как нарушение инварианта.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 16:01:32 +03:00
Nick ShirokovandClaude Opus 5 26ca7276e6 test(skills): expected-skips.mjs — ожидаемое число пропусков вместо запомненного
«skipped» — норма, а не падение, но само число ни о чём не говорит, пока не с чем сверить,
а запоминать его нельзя: оно растёт с набором кейсов. Прежняя сверка жила в личной памятке
как grep по 'external:|runtimeOnly|osOnly' и уже сломалась — она считает скипом ЛЮБОЙ osOnly,
а posix-кейсы фейка платформы на маке как раз выполняются (grep давал 67 против 59 реальных).

Скрипт повторяет правила гейтинга раннера и живёт рядом с ним, поэтому расходиться им негде.
--list печатает пропуски поимённо с причиной. Сверено: win32/powershell 8, win32/python 11,
darwin/python 59 — совпало с прогонами.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:55:07 +03:00
Nick ShirokovandClaude Opus 5 64c629a897 test(skills): sh-фейк снимает кавычки со значения /Out
Значение /Out несёт кавычки внутри токена — это соглашение 1С (run_v8 передаёт
File="путь" так же). В batch их снимает %~2, поэтому win32-фейк работал; в sh
их надо снять явно, иначе cp целится в имя файла с кавычками и лог не появляется.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:47:41 +03:00
Nick ShirokovandClaude Opus 5 793cb8f3d6 test(skills): фейк платформы для *nix — детектор тихих отказов проверяется и на macOS
Кейсы детектора были написаны на batch-фейке и потому гейтились osOnly: win32. На маке
py-порт единственный, то есть ровно там, где проверять важнее всего, детектор оставался
без автоматического покрытия.

Добавлен sh-фейк и восемь зеркальных кейсов (osOnly: darwin/linux, runtimeOnly: python).
Для этого потребовались два расширения DSL:

- `writeFile.executable` — на *nix навык запускает платформу через exec, и файл без бита
  исполнения не стартует вовсе; Node пишет файлы без +x. На Windows chmod — no-op.
  Реализовано во всех трёх местах разбора шага (оба в runner.mjs и в verify-snapshots.mjs):
  ключ, который понимает только один раннер, даёт тихую дыру.
- `osOnly` принимает массив, а не только строку. Иначе darwin и linux требовали бы двух
  копий одного кейса.

Оба ключа описаны в tests/skills/README.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:46:47 +03:00
Nick ShirokovandClaude Opus 5 a024b7da9c fix(db-load-git,db-update): ловить тихие отказы платформы, как db-load-xml
Платформа рапортует об успехе (exit 0) и одновременно пишет в /Out-лог, что часть
метаданных отброшена. Детектор этого был только в db-load-xml, хотя db-load-git гоняет
тот же /LoadConfigFromFiles (и дописывает /UpdateDBCfg в тот же вызов), а db-update —
вторую половину той же цепочки. Оба лог печатали, но не разбирали.

Детектор извлечён в Find-SilentRejections / find_silent_rejections и внесён в реестр
семей check-inline-drift: инлайн-код гард сверять не умеет, а именно расхождение копий
и было бы главным риском такого дублирования.

Добавлен восьмой паттерн — «Для работы с конфигурацией необходима версия платформы не
меньше». Замерено при работе над issue #63: конфигурация с режимом совместимости выше
платформы грузится с кодом 0, db-update тоже отвечает 0, объекты в базу не попадают, а
отказ приходит только в рантайме. Строка обрезана до инвариантной части — конкретная
версия в сообщении меняется.

Текст предупреждения переписан. Убрана подсказка «pass -StrictLog to treat as error»:
ключ предназначен для регрессов (его передаёт verify-snapshots), а совет бессмысленный —
операция уже выполнена, и повторять её ради того же текста незачем. Формулировка больше
не утверждает «dropped properties/refs»: класс проблемы разный, а строки лога печатаются
следом и говорят за себя.

Две правки по дороге:

- `return ,$found` в паре с `@()` у вызывающего давал массив из одного пустого массива,
  то есть предупреждение «1 problem(s)» на чистом логе. Возврат без запятой-обёртки.
- py-порт писал предупреждение в stderr, PS1 — в stdout. В самом py-порте stderr занят
  исключительно фатальными «Error:» перед exit 1, так что не-фатальное предупреждение там
  было единственным исключением. Приведено к stdout — и к соглашению своего же файла, и к
  поведению PS1; verify-snapshots на падении читает `stderr || stdout`, поэтому диагностика
  не теряется.

Тесты: фейковая платформа .cmd, которая вычитывает путь из /Out и кладёт туда готовый лог,
— по четыре кейса на db-load-xml и db-update (отбраковка, она же под -StrictLog, новый
паттерн, чистый лог без предупреждения). Раньше детектор не был покрыт вообще.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:14:36 +03:00
Nick ShirokovandClaude Opus 5 06f21ab3d1 test(skills): гард проверенного диапазона версий формата
Допустимый список версий был независимым литералом в десяти файлах, и сверять его
было не с чем. Именно так волна 2.21 прошла по четырём валидаторам и молча обошла
пятый — form-validate остался на 2.17–2.20 (issue #63).

check-format-versions.mjs держит три инварианта: границы диапазона одинаковы во всех
навыках и на обоих портах; дефолт -FormatVersion у *-init лежит внутри диапазона;
верхняя граница совпадает с последней ЗАМЕРЕННОЙ ступенью таблицы §7.1 из
1c-configuration-spec.md — так расхождение спеки и кода падает здесь, а не на чужой
выгрузке. Отдельно ловится возврат ValidateSet/choices в *-init.

Get-FormatRank разъехался бы по восьми новым копиям — они внесены в реестр семьи
format_rank в check-inline-drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:21:07 +03:00
Nick ShirokovandClaude Opus 5 beaa75c742 fix(epf-build): версия и режим совместимости заглушки — из исходников
Заглушечная конфигурация зашивала version="2.17" и CompatibilityMode=Version8_3_24
независимо от собираемых исходников. Ограничение платформы одностороннее — она читает
формат не новее себя, — поэтому на 8.3.20 такая заглушка не грузилась вовсе:
«Неизвестная версия формата 2.17 загружаемого файла».

Режим совместимости бил тише и потому опаснее: платформа рапортовала успешную
загрузку с кодом 0, писала в лог «Для работы с конфигурацией необходима версия
платформы не меньше, чем 8.3.24» и не создавала объекты — сборка падала уже на
«Неизвестное имя типа», уводя диагностику в сторону.

Оба значения выводятся из версии исходников. Для версии взят min(исходники, 2.17):
заглушке нужна самая низкая работающая версия, а не версия исходников, поэтому на
2.17+ поведение остаётся прежним и опускается только под 2.13–2.16. Режим — по той же
лестнице.

Проверено на живой платформе: обработка 2.13 со ссылочным реквизитом собирается на
8.3.20 обоими портами; для исходников 2.17 заглушка по-прежнему 2.17/Version8_3_24.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:20:55 +03:00
Nick ShirokovandClaude Opus 5 de76fde275 fix(*-validate,*-init): проверенный диапазон версий формата 2.17–2.21
Валидаторы объявляли «ожидаемым» литеральный список версий и ругались на всё
остальное как на подозрительное. Но 2.13–2.16 не подозрительны — они просто не
проверялись, а form-validate вдобавок отстал на 2.21 и предупреждал о форме, которую
сам же создаёт цепочкой epf-init 2.21 → form-add (issue #63).

Теперь сравнение числовое, через общий Get-FormatRank, и исходов три: ниже 2.17 и
выше 2.21 — предупреждение «not verified on it», нечисловое значение — ошибка. Раньше
мусор вида «abc» и реальная версия 2.16 давали одно и то же предупреждение.

В *-init ValidateSet/choices сняты: версия вне диапазона больше не запрет, а
предупреждение в stderr — скаффолд выпускается. Опечатка (2,17) остаётся ошибкой.
Предупреждение печатается напрямую в stderr и после настройки кодировки консоли:
Write-Warning в PS 5.1 уходит в stdout, получает локализованный префикс и перенос по
80 символов, а до [Console]::OutputEncoding em-dash уезжал в вопросы — оба расхождения
ломали паритет с py-портом.

Отдельно в cf-init исправлен гейт TextToSpeech: свойство приезжает форматом 2.18
(8.3.25), а не 2.21. Замер: выгрузки пустой ИБ шести платформ — 37 записей
UsedMobileApplicationFunctionalities на 2.13/2.17 и 38 на 2.18–2.21. Ролики загрузки
показали, что это настоящее новое свойство, а не смена дефолта эмиссии: на 2.17 тег
роняет загрузку XDTO-ошибкой (8.3.24), значение значимо (true переживает роундтрип),
а пропуск на 2.18+ разводит роундтрип — платформа допишет тег сама. Снапшоты трёх
кейсов meta-compile обновлены: их фикстуры строятся cf-init, дельта — только вставка
тега.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:20:40 +03:00
Nick ShirokovandClaude Opus 5 35c8618813 test(skills): expect.stderrContains и захват stderr на успешном прогоне
execSkillAsync отдавал только stdout, а stderr сохранялся исключительно в ветке
ошибки. Поэтому предупреждение навыка, который отработал успешно (exit 0), проверить
было нечем: кейс мог убедиться лишь в том, что навык не упал, — то есть в молчании
вместо текста.

Резолв теперь отдаёт оба потока, добавлен ключ expect.stderrContains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:20:19 +03:00
Nick ShirokovandClaude Opus 5 9949039d98 docs(1c-*): лестница версий формата — по одной на релиз платформы
Утверждение «2.17 = платформы 8.3.20–8.3.24» повторялось в 7 местах и было неверным:
точка «8.3.20» получена не замером, а из имени каталога cfsrc/acc_8.3.20, который
побайтово совпадает с acc_8.3.24 (91807/91807 файлов; обе — БП 3.0.181.31 с режимом
Version8_3_24, который 8.3.20 открыть не может). Интерполяция между двумя одинаковыми
точками и дала мнимый интервал.

Замер пустыми ИБ на шести установленных платформах (создать базу платформой X и
выгрузить ею же): 8.3.20 → 2.13, 8.3.24 → 2.17, 8.3.25 → 2.18, 8.3.26 → 2.19,
8.3.27 → 2.20, 8.5.1 → 2.21. Версия меняется каждым релизом.

Полная лестница теперь в одном месте — §7.1 1c-configuration-spec.md, с колонкой
«замерено»: 8.3.21/8.3.22 не проверялись, 2.16 подтверждена только сторонним
сообщением. Остальные спеки на неё ссылаются.

Заодно сняты два вывода, выведенных из каталога-дубля: «содержимое Form.xml идентично
между 8.3.20 и 8.3.24» и строка «8.3.20 | 2.17 | Базовая» в спецификации ролей.
Дописан 2.21 в таблицы, которые о нём не знали, и уточнён TextToSpeech: он приезжает
в 2.18, а на 2.17 роняет загрузку XDTO-ошибкой, а не отбраковывается молча.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:20:10 +03:00
Nick Shirokov f522d29cc0 fix(cfe-borrow): не переносить FoldersOnTop в оболочку заимствованного объекта
Сквозной прогон (собрали расширение навыками → загрузили → обновили БД →
выгрузили обратно) показал: записанный нами <FoldersOnTop> из выгрузки
пропадает. Платформа его у заимствованной оболочки не хранит — принимает
молча и выбрасывает. Соседние свойства того же списка (Hierarchical,
CodeLength, DescriptionLength, CodeType, CodeAllowedLength) сохраняются,
так что дело именно в этом свойстве. Эталон Конфигуратора его тоже не
переносит.

Список propsToExtract собирался на глаз при появлении -BorrowMainAttribute;
это второе свойство из него, которое платформа не принимает (первым был
NumberPeriodicity, ронявший UpdateDBCfg).

Одноразовый прогонщик сценариев — debug/roundtrip/ (в .gitignore).
Регресс cfe-borrow 12/12 на обоих рантаймах, гарды зелёные.
2026-08-12 20:34:13 +03:00
Nick Shirokov af00aa4711 fix(form-validate,cfe-borrow): остаточные ложные ошибки на формах платформы
Корпусный прогон (УТ/БП/ERP, 21 097 форм) после понижения Command/Action
оставлял 8 форм с ошибками. Разобраны все, дефектов оказалось два.

1. Вложенная таблица. Путь Items.<Таблица>.CurrentData.<Поле> разрешался
   ОДНИМ шагом: если таблица сама привязана через Items.*, корнем
   оставался литерал «Items», и типовая форма объявлялась битой
   (НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP). Теперь
   разрешение идёт цепочкой, со страховкой от кольца ссылок.

2. AutoCommandBar с обычным id вместо -1. Это соглашение, а не требование:
   21 094 формы из 21 097 используют -1, но три платформа выгружает с
   обычным id и грузит их без нареканий. Понижено до предупреждения;
   ошибка осталась на случай, когда id вообще не число.

Оставшиеся три формы — дубли id элементов и команд. Это НЕ ложные
срабатывания: измерение по корпусу показало ровно по одному случаю на
21 097 форм, то есть опечатки вендора, а не структурное правило (были бы
пулы id раздельными, пересечений были бы тысячи). Плюс кейс duplicate-id
прямо требует считать дубль ошибкой.

Итого по корпусу: 283 формы с ошибками → 3.

Заодно дооформлена фикстура cfe-borrow/container-types (пространство имён
веб-сервису, документ журналу и последовательности): verify-snapshots по
cfe-borrow теперь 12/12.
2026-08-12 20:20:02 +03:00
Nick Shirokov 71f047291c fix(form-validate): версия формата — общим helper-ом, с учётом внешних обработок
Проверка версии читала Configuration.xml собственной регуляркой. Отсюда два
пробела: форма автономной внешней обработки не проверялась вовсе (своего
Configuration.xml у неё нет, версию несёт корень обработки), а разбор
дублировал уже существующий эталон.

Теперь используются копии общих эталонов — detect_format_version (авторитет
form-compile) и support-guard: is_external_root (авторитет cf-edit), оба
навыка добавлены в реестр check-inline-drift.

Попутно вскрылось следствие для соседней проверки: обработка, чьи исходники
лежат внутри дерева с Configuration.xml (обычная раскладка src/cf рядом с
src/epf), считалась «конфигурационным контекстом», и Check 12 ругался на её
собственные External*-типы. Теперь климб останавливается на ближайшем
якоре — том же правиле границы автономного объекта, что в support-guard.

Плюс паритет: PS отчитывался «Data bindings: none», PY эту ветку не имел.

Кейс на EPF-контекст добавлен. Регресс 16/16 на обоих рантаймах, корпус
21 097 форм без новых срабатываний, гарды зелёные.
2026-08-12 19:28:11 +03:00
Nick Shirokov 7dc2e6e443 feat(form-validate): проверки необъявленного префикса и версии формата
Обе проверки — про ошибки, которые делает не платформа, а тот, кто пишет
XML руками. Обе вскрыты на наших же фикстурах, обе платформенно-фатальны:
файл не читается вовсе, а прежний валидатор говорил OK.

Check 13 — префикс в значении типа обязан резолвиться. `cfg:CatalogRef.X`
в <v8:Type> при незадекларированном xmlns:cfg даёт «Исключение XDTO при
чтении файла». Область видимости считается по узлу, а не по корню:
локальная xmlns на элементе законна и в типовых встречается (d4p1, mxl).

Check 14 — версия формата формы против версии конфигурации. В пределах
одной выгрузки версия едина; форма из более новой выгрузки даёт
«Неизвестная версия формата N загружаемого файла».

Ложных срабатываний нет: корпус УТ/БП/ERP, 21 097 форм — те же 8 форм с
ошибками, что и до правки. Регресс 15/15 на обоих рантаймах, паритет
портов сверен построчно.
2026-08-12 18:59:55 +03:00
Nick Shirokov 1b40dc5b03 test(form-validate): фикстуры доведены до загружаемых конфигураций
verify-snapshots считает фикстуру готовой конфигурацией и грузит её в базу,
а фикстуры form-validate состояли из одного Form.xml — поэтому по этому
навыку верификация падала целиком и по факту не выполнялась никогда.

Что вскрылось при доведении, по нарастающей:

1. Нет Configuration.xml и объекта — «Файл объекта не существует».
   Дописаны cf-init + meta-compile + form-add, рукописный Form.xml сохранён.
2. Префикс `cfg:` в значении <v8:Type> при НЕобъявленном xmlns:cfg —
   «Исключение XDTO произошло при чтении файла». Тот же класс, что ишью #38,
   только в наших собственных тестовых данных: четыре фикстуры платформа не
   читала вовсе, а кейсы на них считались зелёными.
3. Привязка Объект.Наименование у обработки — у неё нет такого стандартного
   реквизита. Добавлен обычный реквизит.
4. Динамический список без источника — «Неверный путь к данным: Список.Ссылка».
   Добавлен справочник-источник и MainTable.

Кейс с формой расширения помечен skipPlatformVerify: верификатор грузит
каталог кейса как конфигурацию, фрагмент расширения так не проверить. Ключ
существовал в verify-snapshots, но не был описан — добавлен в README.

form-validate: 14/14 на обоих рантаймах, платформенная верификация 13/14
(один пропуск с причиной, было 10/14 с четырьмя падениями).
2026-08-12 18:36:09 +03:00
Nick Shirokov e1d5d1f903 fix(form-validate): команда без Action — предупреждение, а не ошибка
Проверка объявляла ошибкой любую команду формы без <Action>. Корпусный
прогон (УТ 8.3.27, БП 8.3.27, ERP 8.3.24 — 21 097 форм) показал 406 таких
команд на 275 формах, и все они произведены самой платформой: формат это
допускает, конфигурации грузятся.

Приём типовых: действие назначается в рантайме, в ПриСозданииНаСервере —
`Команда.Действие = "Подключаемый_" + Имя + "Локализация"`. Назначать может
и чужой модуль (переопределяемый слой, подключаемые команды), поэтому по
одному Form.xml вердикт не вынести — отсюда предупреждение, а не ошибка, и
никакого подглядывания в соседний модуль.

Корпус после правки: форм с ошибками 283 → 8, новых срабатываний нет.
Оставшиеся 8 — четыре других класса, разбираются отдельно.

Кейс с фикстурой: команда без Action на кнопке → предупреждение, exit 0.
2026-08-12 17:24:34 +03:00
Nick Shirokov c9b64f3a0d fix(form-compile): внятный отказ на группу additionalColumns без ключа columns
PS-порт падал с «Не удается индексировать в массив NULL»: @($null).Count в
PowerShell равен единице, поэтому ветка самозакрывающегося тега была
недостижима, и код шёл эмитить несуществующую колонку. PY на том же входе
молча писал пустую группу — портируемого поведения не было вовсе.

Разведены два случая, которые до сих пор путались:
- `"columns": []` — явно пустая группа, законная форма (платформа так
  пишет таблицу без доп. колонок). Работает как работала, self-closing;
- ключа `columns` нет вовсе — недосказанность автора: «доп. колонки есть»,
  а какие, не сказано. Теперь отказ с указанием, что делать.

Валидатор такое по-прежнему пропускает: в выгрузке платформы пустая группа
встречается, запрет на авторинг не равен запрету на существование.

Заодно кейс cfe-borrow/form-main-attr-columns вернулся к форме эталона
Конфигуратора — таблица без колонок описана явно пустым списком, как в
выгрузке, а не обходным манёвром вокруг падения.
2026-08-12 17:01:10 +03:00
Nick Shirokov a6cb656a2d fix(form-validate): паритет портов по счётчику проверок
На одной и той же форме PS сообщал 12 проверок, PY — 9. Расходился не
вердикт, а учёт: три проверки (ссылки команд, обработчики событий,
действия команд) в PS отчитываются строкой «none», когда проверять нечего,
а в PY эта ветка отсутствовала — и проверка не попадала в счётчик.

Добавлены недостающие ветки. Выборка из 40 форм корпуса (УТ, БП, ERP):
число строк отчёта совпадает у портов на всех сорока.
2026-08-12 16:55:28 +03:00
Nick Shirokov 53ee51a37a fix(cfe-borrow): не переносить NumberPeriodicity в оболочку заимствованного документа
Платформа считает это свойство модификацией настроек нумерации и тогда
требует объявить ещё и <Numerator/>: /UpdateDBCfg падает с «Для
заимствованного документа, настройки нумерации которого модифицированы,
отключать контролируемость свойства "Нумератор" недопустимо». Загрузка при
этом проходит — ошибка вылезает только на обновлении конфигурации БД,
поэтому ручной E2E её не видел, а verify-snapshots поймал.

Эталон Конфигуратора переносит NumberType/NumberLength/NumberAllowedLength
и не переносит NumberPeriodicity — приводим к тому же. Свойство попало в
список ещё при появлении -BorrowMainAttribute, набор тогда был угадан, а
не выверен по эталону.

Заодно две мои фикстуры доведены до платформенной валидности:
form-choice-param-links задавала в ИСХОДНОЙ форме ссылку на несуществующий
реквизит — такая конфигурация не грузится сама, проверять на ней вырезание
висячей связи нельзя; gentypes-full не имела регистратора для регистра
бухгалтерии и задачи для бизнес-процесса.

verify-snapshots по cfe-borrow: 11 из 12 (container-types падал и раньше,
дефект его фикстуры). Регресс 12/12 на обоих рантаймах, все гарды зелёные.
2026-08-12 16:52:42 +03:00
Nick Shirokov 9650085fff docs(cfe-init): каталог расширения в примерах повторяет его имя
В примерах имя расширения задано явно (-Name Расш1), а рядом стоял
плейсхолдер -OutputDir src\cfe\extname — связь между именем и каталогом
из такого примера не читается. Теперь каталог называется по имени, и
конвенция «подкаталог = имя расширения» видна из самих примеров.

Базовая команда дополнена -OutputDir и -ConfigPath: без них расширение
уезжает в src (тот самый неоднозначный каталог), а совместимость и UUID
языка берутся по умолчанию вместо базовой конфигурации.
2026-08-12 16:29:16 +03:00
Nick Shirokov c0c37532a7 docs(cfe-*): единая конвенция путей в примерах и передача ConfigPath валидатору
В группе cfe-* примеры расходились: cfe-patch-method уже использовал
src\cfe\<расширение> и src\cf, остальные три навыка — голый src и
абсолютный C:\cfsrc\erp. Голый src особенно вреден: в репозитории с
конфигурацией и расширением он неоднозначен, и модель подставляет в
-ExtensionPath конфигурацию.

Везде одна пара: src\cfe\extname и src\cf.

Заодно по итогам появления -ConfigPath у cfe-validate: блоки «Верификация»
теперь показывают его передачу, а описание параметра в самом валидаторе
переписано с устройства проверки на повод её включить — с последствием
(расширение пройдёт валидацию и будет отвергнуто платформой) и с
алгоритмом поиска пути прямо на месте, без отсылки к соседнему навыку.

У cfe-init отмечено, что CompatibilityMode не нужен при заданном
ConfigPath, и что расширение стоит класть в отдельный подкаталог.
2026-08-12 16:24:57 +03:00
Nick Shirokov 9b1c3de642 feat(cfe-validate): полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath
Три вещи, на которых платформа отвергала расширение, а валидатор молчал.

Check 9 — полнота набора GeneratedType у заимствованной оболочки: неполный
набор платформа не читает («отсутствует один или более типов объекта»).
Карта категорий взята из той же таблицы спецификации (§2.5) и заведена в
реестр check-type-maps.mjs, чтобы копия не разошлась с остальными.

Check 12 — <AdditionalColumns table="Объект.X"> при незаимствованной
табличной части. В отличие от соседних проверок блока, здесь сигнал точный
(имя из атрибута), а последствие жёсткое, поэтому ошибка, а не warning.

Check 14 — пути Объект.* заимствованных форм против конфигурации-источника,
по новому опциональному -ConfigPath. Без него проверка пропускается с явной
строкой в отчёте. Отличить живой путь от висячего иначе нельзя: Объект.Партнер
валиден и без заимствования (наследуется от базы), а Объект.Товары.Артикул не
разрешится нигде. Итоги колонок (Total<Колонка>) и стандартные реквизиты
пропускаются — иначе ложные срабатывания на типовых формах.

Проверено на пяти расширениях: наши (оба режима) и оба эталона Конфигуратора
проходят чисто, расширение с дефектом ловится. Регресс cfe-* 51/51 на обоих
рантаймах, гарды дрейфа зелёные.
2026-08-12 16:17:56 +03:00
Nick Shirokov 9a28fbfacb feat(form-validate): путь на необъявленный основной реквизит заимствованной формы
Форма, которую платформа отвергала с «Неверный путь к полю - Объект.Партнер»,
проходила валидацию с вердиктом OK. Check 5 такое не видит по двум причинам:
у формы с BaseForm он пропускает базовые элементы (id < 1000000), а привязка
внутри <ChoiceParameterLinks> лежит в <xr:DataPath> и в его список тегов не
входит вовсе.

Проверка 11d: если форма не объявляет основной реквизит, любой путь с корнем
«Объект» не разрешится — ошибка. Непрозрачные формы пути (1/0:uuid), которыми
как раз и заменяет такие ссылки Конфигуратор, ошибкой не считаются.

Две фикстуры: висячая ссылка ловится, та же форма в uuid-форме проходит.
Регресс 13/13 на обоих рантаймах.
2026-08-12 15:58:45 +03:00
Nick Shirokov 1bd0c7724e feat(cfe-borrow): ссылки параметров выбора по uuid реквизита
Заимствование формы без основного реквизита копировало
<ChoiceParameterLinks>/<xr:Link> как есть, с текстовым путём
«Объект.Партнер». В расширении такой путь не разрешается — платформа
отвергала загрузку: «Неверный путь к полю - Объект.Партнер». Привязка
лежит в <xr:DataPath> внутри xr:Link, и общий стриппинг её не видел.

Конфигуратор ссылку не выбрасывает, а переводит путь в непрозрачную форму
«1/0:<uuid реквизита объекта>» — связь остаётся рабочей. Делаем так же;
реквизит, которого в источнике нет, недоступен и по uuid — такую связь
вырезаем вместе с опустевшим контейнером. Путь односегментный во всём
корпусе УТ (285 из 285), глубже не бывает.

Результат совпал с эталоном Конфигуратора вплоть до uuid, E2E на UT_DEMO:
«Load completed successfully». Регресс 12/12 на обоих рантаймах.
2026-08-12 15:53:59 +03:00
Nick Shirokov 3fafa099b2 feat(cfe-borrow): перенос UseAlways/Columns основного реквизита формы
Заимствование формы с -BorrowMainAttribute синтезировало <Attribute
name="Объект"> с нуля — Type/MainAttribute/SavedData и всё. Секции
<UseAlways> и <Columns> исходной формы терялись, а вместе с ними и
дополнительные колонки табличных частей, объявленные прямо в форме:
платформа отвергала загрузку («Неверный путь к данным: Объект.Товары.Артикул»).

Три связанных следствия:
- секции переносятся из исходной формы (helper Get-MainAttributeExtraXml
  на оба порта, отступ в BaseForm — тем же приёмом, что у ChildItems);
- табличная часть, упомянутая только в <AdditionalColumns table="…">,
  теперь попадает в сбор путей и заимствуется — иначе «Колонки не могут
  быть добавлены к реквизиту»;
- типы колонок дозаимствуются через collect_reference_types, как это
  делает Конфигуратор (DefinedTypes/Артикул в эталоне).

Попутно снят PS↔PY-дрейф: lxml включал хвостовой пробельный узел в
tostring, из-за чего .py вставлял пустые строки, которых нет у .ps1
(with_tail=False в четырёх местах).

E2E на UT_DEMO (8.3.27, формат 2.20, форма заказа поставщику): режим с
основным реквизитом грузится «Load completed successfully» без ручных
правок. Регресс 11/11 на обоих рантаймах.
2026-08-12 15:49:34 +03:00
Nick Shirokov 902b643a3e feat(cfe-borrow): полный набор GeneratedType у заимствованных оболочек
Платформа отвергала заимствованный план видов характеристик: «отсутствует
один или более типов объекта ChartOfCharacteristicTypes». В карте
$script:generatedTypes не хватало категории Characteristic — а вместе с ней
ещё пяти категорий у пяти типов (планы счетов и видов расчёта, регистры
бухгалтерии и расчёта, бизнес-процессы).

Дефект был чистым дрейфом копий: карта живёт в трёх навыках, и в
meta-compile с meta-validate она верна. Чтобы расхождение больше не
копилось молча, каноническая таблица наборов GeneratedType заведена в
спецификации (§2.5), а check-type-maps.mjs получил виды gentypes/gencats и
сверяет по ней все три карты.

Регресс cfe-* 47/47 на обоих рантаймах, оба гарда дрейфа зелёные.
2026-08-12 15:38:52 +03:00
Nick ShirokovandClaude Opus 5 37108b15c4 test(skills): свести verify-snapshots с runner по разбору кейса
Верификатор читает тот же DSL, что и функциональный раннер, но своей
реализацией, и отставал на пять ключей: preRun[].cwd, inputFrom, cwd на уровне
кейса, раскрытие {workDir} в args_extra и маппинг from: outputPath. Кейсы,
опирающиеся на них, до платформы не доезжали вовсе — падали на подготовке
фикстуры, причём шаг preRun с относительным путём писал её в корень репозитория.

Незнакомый from теперь роняет кейс с внятным сообщением: молчаливый default
маскировал расхождение под дефект навыка (флаг уходил без значения).

Платформенная верификация: mxl-compile 28/36 -> 36/36, mxl-decompile 0/7 -> 7/7,
mxl-info 2/7 -> 7/7, mxl-validate 0/4 -> 4/4, skd-decompile 0/17 -> 17/17.

Оба ключа, которых не было в документации DSL, дописаны в README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:40:18 +03:00
Nick ShirokovandClaude Opus 5 241a56a29f docs(mxl): актуализировать спецификации после серии находок на стенде
Спецификация XML — дописано то, что вскрыли контролируемые макеты и замеры
по корпусу:

- шрифт-ссылка на СИСТЕМНЫЙ шрифт: префикс sys в корне не объявлен, поэтому
  объявление xmlns дописывается прямо на узел. Плюс правило вывода kind
  из префикса и то, что неиспользуемый шрифт в палитру не попадает;
- новый раздел «Устройство палитр»: порядок документный и НЕ зависит от
  последовательности действий автора (проверено опытом с оформлением снизу
  вверх), формат по умолчанию последний, палитра дедуплицирована по содержимому;
- у текста ячейки ТРИ состояния: тега нет, тег с элементами, пустой <tl/>.
  Третье — 57% макетов корпуса;
- языковые настройки: набор языков не выводится из языков текста, description
  бывает самозакрывающимся, currentLanguage бывает отсутствующим и бывает
  указывающим на необъявленный язык.

Спецификация DSL — из ограничений убрано объявление языков макета: оно больше
не теряется. Добавлено пояснение, почему побайтовое совпадение достижимо не на
любом макете: в долго правленных макетах остаются следы прежних состояний,
которые из итогового документа не выводятся.

В инструкции навыка отражена только форма шрифта-ссылки — остальное из этой
серии либо уже там, либо для авторинга не нужно.

Примеры из справочника скомпилированы и проверены валидатором.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:53:10 +03:00
Nick ShirokovandClaude Opus 5 98bcf79b41 feat(mxl-compile,mxl-decompile): шрифт ссылкой на стиль или системный
Шрифт бывает не собственным описанием, а ссылкой: на элемент стиля конфигурации
(style:) или на системный шрифт (sys:). Своих атрибутов у такого шрифта нет, и мы
превращали его в пустую запись — faceName="" height="0". ps1 при сборке подставлял
туда Arial 10, то есть подменял данные молча.

В корпусе таких шрифтов 272 в 213 макетах из 10 924: StyleItem 209, WindowsFont 63.

Запись — та же, что у шрифта в описании формы: { "ref": "style:TextFont" }.
kind выводится из префикса, ключом быть не обязан.

Синтетический стенд показал деталь, которую по корпусу было не разглядеть: префикс
sys в корне документа не объявлен, поэтому платформа дописывает объявление xmlns
прямо на узел шрифта — тот же приём, что с цветами из web-палитры.

Порты после этого сошлись ПОЛНОСТЬЮ: на пилоте из 40 макетов совпадают и JSON
декомпиляторов, и собранный XML. Макет со шрифтами добавлен в побайтовую
регрессию, стенд 8 из 13.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:47:24 +03:00
Nick ShirokovandClaude Opus 5 0a8671a166 feat(mxl-compile,mxl-decompile): пустой тег текста ячейки
У текста ячейки три состояния, а выражались два: тега нет вовсе, тег с пустым
текстом на каждый язык — и третье, <tl/>, которое мы теряли. Это не редкость:
38 075 ячеек на 1200 макетов, встречается в 57% макетов корпуса.

Выражается пустым объектом: "text": {}. Не новый ключ и не новое понятие, а
вырожденный случай уже принятой записи «язык → текст» — языков нет вовсе.
В позиционной строке двусмысленности не создаёт: {} не несёт ключей ячейки,
значит по общему правилу читается как текст, а null там по-прежнему «пропустить
колонку».

В описании DSL записи нет намеренно. Для авторинга она бесполезна — визуально
это тот же пустой текст, что и "", — а документировать две пустые формы рядом
значило бы завести развилку, у которой нет правильного ответа.

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

На пилоте потери в категории row[].cell[].tl упали с 12 488 до 2, заодно
row[].cell[].fmt с 41 645 до 30 088; совпадение строк документа 17.6% → 18.5%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:21:02 +03:00
Nick ShirokovandClaude Opus 5 6619a4e7ad fix(mxl-compile,mxl-decompile): висячие ссылки на стиль и культурная сортировка
Два дефекта, найденных при сведении портов.

Стиль, на который ссылалась только дополнительная колоночная раскладка,
отсекался как неиспользуемый: проверка смотрела в result, а columnSets
попадает туда ПОЗЖЕ неё. Ссылка оставалась висячей — 5 макетов пилота
из 40 указывали на стиль, которого в styles нет. Берём стили из самих
раскладок.

Именованные элементы платформа хранит отсортированными по имени ординально.
ps1 сортировал их Sort-Object -CaseSensitive, а он всё равно сравнивает по
текущей культуре — комментарий рядом сам об этом предупреждал. На кириллице
порядок расходился с py-портом (4800 строк разницы на одном макете).
Сортируем по ключу из кодов символов: в нём только 0-9A-F, культура его
переупорядочить не может.

Порты сошлись: JSON декомпиляторов совпадает на всех 40 макетах пилота
(было 26 расхождений), собранный XML — на 39 из 40 (было 20 расхождений).
Остаток — один макет, где расходятся компиляторы: ps1 подставляет
faceName="Arial" height="10" там, где py пишет пустой шрифт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:03:53 +03:00
Nick ShirokovandClaude Opus 5 c11f4837fd fix(mxl-decompile): свести порты — пробелы, порядок и свойства значения ячейки
Расхождение JSON между портами было 26 макетов из 40, стало 5. Расхождение
собранного XML — 20 из 40, стало 7. Причин оказалось четыре, и три из них
дефекты, а не просто разнобой.

1. ps1 грузил XML с PreserveWhitespace = $false, и текст ячейки из одних
   пробелов схлопывался в пустой. Молчаливая потеря данных в каноничном
   порте, 25 макетов пилота.

2. Стили обнаруживались обходом хэш-таблицы строк, а её порядок в PowerShell
   НЕ определён. Платформа же кладёт записи палитры в порядке документа, так
   что порядок обхода — часть верности вывода, а не деталь. Обход теперь по
   возрастанию номера строки. ([ordered] тут не годится: с целочисленными
   ключами он индексируется по позиции, а не по ключу.)

3. Именованные области сортировались нестабильным Sort-Object (-Stable
   появился только в PowerShell 6.2), поэтому области с одинаковыми границами
   получали произвольный порядок. Добавлен явный ключ исходного порядка —
   в оба порта, чтобы совпадение было по построению, а не по совпадению.

4. containsValue / valueType / controlType протекали в styles сквозным
   пробросом неизвестных тегов. Это свойства ЗНАЧЕНИЯ ячейки, а не оформления,
   и компилятор таких ключей не знает — в DSL они были чистым шумом. Заодно
   вложенный элемент больше не читается как скаляр: ps1 брал InnerText и
   получал склейку поддерева, py брал .text и получал пустоту.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:40:44 +03:00
Nick ShirokovandClaude Opus 5 e46db618e3 docs(mxl): уточнить, как читается объект в позиционной записи строки
Прежняя формулировка противопоставляла «ячейку» и «многоязычный текст», хотя
текст тоже даёт ячейку. Разница не в этом: объект либо описывает СВОЙСТВА
ячейки (среди ключей есть ключ её схемы), либо является её ЗНАЧЕНИЕМ, и тогда
ключи — идентификаторы языков.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:16:17 +03:00
Nick ShirokovandClaude Opus 5 b5fef09844 feat(mxl-compile,mxl-decompile): многоязычный текст элементом строки-массива
В позиционной записи строки элемент — это значение содержимого ячейки, а значение
текста по общей конвенции бывает строкой либо объектом «язык → текст». Значит
объект {ru, en} там законен так же, как строка, и новой формы это не заводит.

Раньше ячейка с многоязычным текстом не считалась «простой», поэтому строка
двуязычного макета оставалась объектной:

  { "cells": [{ "text": { "ru": "Пусто", "en": "Empty" } }, { … }] }
  [{ "ru": "Пусто", "en": "Empty" }, { … }]

Объект-элемент читается как ячейка, если несёт хоть один её ключ, и как текст
в противном случае: идентификаторы языков с ключами ячейки не пересекаются
(в корпусе это ru, en, ru1, Русский).

Попутно исправлен признак «список позиционный»: он искал элемент-строку, поэтому
строка из одних многоязычных текстов позиционной не признавалась и ключ cells
у неё оставался.

Правка не должна менять скомпилированный XML — и не меняет: пилот из 40 макетов
собрался побайтово так же, JSON при этом изменился у 13. Стенд 7 из 10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:13:32 +03:00
Nick ShirokovandClaude Opus 5 f926acd4f4 fix(mxl-decompile): убрать из вывода бессмысленный style и лишние объектные строки
Две правки читаемости DSL, обе замечены на живом выводе.

Ячейка получала "style": "default" — ссылку на стиль, которого в styles нет
вовсе. Условие сверялось с обнаруженным стилем пустых колонок, а он бывает
равен "default": строка такой стиль не пишет, а ячейка получала ключ на
пустое место. Теперь сверяемся со стилем, который РЕАЛЬНО раздаётся ячейкам.

Позиционная запись строки отбрасывалась, как только первая ячейка стояла не
в первой колонке. Но один-два null впереди обычно короче объектной записи
с col: строка вида { "cells": [{ "col": 2, "text": "…" }] } сворачивается
в [null, "…"]. Выбираем ту форму, которая короче.

Правка не должна менять скомпилированный XML — и не меняет: пилот из 40
макетов собрался побайтово так же, как до неё, при том что JSON изменился
у 31 макета. Стенд по-прежнему 7 из 10 байт в байт.

Попутно блок JSON-сериализатора в ps1 перенесён выше первого использования:
в PowerShell функция должна быть объявлена до вызова.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 20:59:54 +03:00
Nick ShirokovandClaude Opus 5 96e8616bb4 test(mxl): побайтовый раундтрип макетов, произведённых платформой
До сих пор строгая дорожка кампании жила только в отладочных прогонах, и её
некому было защищать: снэпшот сравнивает наш вывод с нашим же прежним выводом,
поэтому дрейф от платформы он не ловит.

Семь макетов, собранных вручную в Конфигураторе, положены в фикстуры. Кейс
декомпилирует такой макет и компилирует обратно, а результат сверяется с
исходником побайтово. Проверено, что сеть кусается: подмена одного тега в
фикстуре роняет кейс.

Раннеру добавлены две ручки:
- expect.filesEqual — сравнение двух файлов побайтово. Снэпшот его не заменяет:
  --update-snapshots молча принял бы расхождение с платформой;
- inputFrom — брать вход из файла в рабочем каталоге, а не из case.input.
  Нужно, когда вход производит preRun: case.input пишется ПОСЛЕ preRun и затёр
  бы его.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 20:27:58 +03:00
Nick ShirokovandClaude Opus 5 fc792bfee6 feat(mxl-compile,mxl-decompile): языки макета и шрифт как у платформы
Контролируемый стенд (upload/epf/МакетТабличныйДокумент) вскрыл четыре
системных расхождения подряд. После правок 7 макетов стенда из 10 проходят
раундтрип БАЙТ В БАЙТ обоими портами — до этого ни один.

1. Объявление языков макета жёстко писалось как «только русский». Это не то же
   самое, что textLanguages: почти во всех макетах ERP объявлен один ru, а текст
   лежит и под ru, и под en, поэтому выводить одно из другого нельзя. Заведены
   недокументированные ключи languages / currentLanguage / defaultLanguage —
   автору они не нужны, нужны раундтрипу. defaultLanguage несём, а не выводим:
   «всегда ru» — наблюдение на четырёх русских типовых, а не правило формата.

2. <font> писался в каждом формате. Платформа пишет его только когда шрифт
   задан: треть форматов корпуса (23 003 из 69 581) обходится без него.

3. Декомпилятор считал шрифт с индексом 0 «обычным» и не писал его в стиль.
   Нулевой шрифт вовсе не обязан быть обычным — в макетах стенда он курсивный
   и жирный, и начертание терялось.

4. Формат, несущий ТОЛЬКО шрифт, выглядел пустым: у ячейки он получал
   зарезервированное имя default и не записывался, у строки терялся целиком.

Остаток на стенде — три макета, и все три про одно: ссылка ячейки на запись
ширины, оставшуюся от прежнего состояния документа. Это не воспроизводится
принципиально, разобрано в docs/1c-spreadsheet-spec.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 20:08:28 +03:00
Nick ShirokovandClaude Opus 5 f72dd544dd fix(mxl-compile): не класть в палитру шрифт, на который никто не ссылается
Мы всегда заводили шрифт Arial 10 по умолчанию, даже когда его никто не
использует. Платформа так не делает: у макета без оформления элемента <font>
нет вовсе. Неиспользуемые шрифты теперь отбрасываются, ссылки перенумеровываются
(индексы шрифтов позиционные).

Проверено на контролируемом стенде: после правки простейший макет расходится
с платформенным ровно одной строкой — объявлением языков в шапке.

Кейс font-fractional-size дополнен: стиль, задающий ТОЛЬКО шрифт по умолчанию,
равнозначен отсутствию оформления (ячейка получает <f>0</f>), поэтому шрифт
остаётся неиспользованным. Чтобы кейс продолжал проверять целый размер, стилю
добавлено второе свойство.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 19:40:39 +03:00
Nick ShirokovandClaude Opus 5 3f80f936ae fix(mxl-compile): формат по умолчанию — последняя запись палитры
Мы регистрировали формат по умолчанию первым, из-за чего вся палитра шла
со сдвигом относительно платформенной. На корпусе он последний в 8285 макетах
из 10 863, первым — в 25.

Подтверждено контролируемым стендом: в макетах, где ширины заданы на уровне
документа, палитра идёт «ширины колонок в порядке колонок, умолчание последним».

Порядок палитры не виден в семантическом диффе (правило palette-index разрешает
ссылки и тем самым его прячет), поэтому мерялся отдельно: на пилоте совпавших
с оригиналом записей с начала палитры стало 467 из 2274 против 23, палитр,
совпавших целиком, — 4 против 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 19:23:18 +03:00
Nick ShirokovandClaude Opus 5 250ec9ef0d docs(mxl): актуализировать спецификацию XML табличного документа
За кампанию XML-уровень изучен заметно глубже, чем был описан. Внесено то,
что проверено на корпусе ERP (10 924 макета) и на контролируемом стенде:

- <i> платформа пишет только при разрыве последовательности;
- <indexTo> схлопывает только ПУСТЫЕ строки — прежняя формулировка «строки
  с одинаковым содержимым» неверна: одинаковых непустых схлопнутых нет ни одной
  при 98 153 несхлопнутых;
- <f>0</f> — у ячейки формата нет вовсе, это не индекс записи;
- канонический порядок тегов внутри <format> (height раньше width);
- единица ширины — 1/8 символа;
- свёртка четырёх одинаковых сторон рамки в <border>;
- цвет — значение с префиксом пространства имён, web/win объявляются прямо
  на узле; в Form.xml те же цвета выглядят как web:/win:;
- формат строки несёт не только высоту, а формат колонки не только ширину;
  оформление строки материализуется и в ячейки, кроме hidden;
- columnsItem с formatIndex 0 и с индексом за пределами size;
- полный список стилей линии вместо Solid/None.

Отдельно описана ловушка: width в формате ЯЧЕЙКИ — устаревшая ссылка, она не
описывает итоговое состояние документа и воспроизведению не подлежит.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 19:08:52 +03:00
Nick ShirokovandClaude Opus 5 ba25aa4a00 feat(mxl-compile,mxl-decompile): формат самой строки
У строки есть собственный формат: платформа хранит в нём скрытие (17 423 вхождения
в корпусе), шрифт (9 216), фон, выравнивания, защиту. Мы писали туда только высоту,
всё остальное теряли.

Разбор на контролируемом стенде показал, что это два разных случая, а не один.
Оформление, применённое к строке целиком, платформа пишет И строке, И каждой
ячейке (backColor: 13 899 ячеек повторяют против 96). Скрытие — только строке
(24 026 против 62 856). Поэтому одного правила «rowStyle красит ячейки» мало.

Теперь rowStyle — стиль строки: по умолчанию ложится и на строку, и на ячейки,
как это делает платформа. Объектная форма { style, apply } задаёт исключения:
"row" — только строке, "cells" — только ячейкам. Модификатор нужен раундтрипу,
в описании DSL его нет.

Скрытие и высота — собственные свойства строки, ключами рядом: у ячейки таких
свойств не бывает, и в её оформление они не попадают.

На пилоте категория row[].formatIndex упала с 1964 до 201 потерянного факта.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 18:36:41 +03:00
Nick ShirokovandClaude Opus 5 ccbd488d63 fix(mxl-compile): у неоформленной ячейки формата нет вовсе
Ячейка без собственного оформления ссылалась на формат по умолчанию. Платформа
в этом случае пишет <f>0</f>, где ноль — не индекс записи, а «формата нет»:
на корпусе так у 170 710 ячеек против 50 635, ссылающихся на умолчание, и
<f>0</f> встречается в 71% макетов.

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

На пилоте верхняя категория row[].cell[].fmt упала с 63 900 до 55 855 фактов,
совпадение строк документа выросло с 16.7% до 17.5%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:32:56 +03:00
Nick ShirokovandClaude Opus 5 08503c72e9 docs(mxl): разделить справочник DSL и описать новые ключи
Справочник был одним файлом на 273 строки, а полная таблица свойств стиля его
бы удвоила. Разделён по частоте обращения, как в meta-compile: в инструкции
маршрутная таблица «что нужно → какой файл».

  reference/dsl-spec.md          — верхний уровень, области, строки, ячейки
  reference/styles.md            — шрифты, стили, цвет, рамка, колонки
  reference/format-properties.md — полный перечень свойств стиля

Описаны columnStyles и новая запись стиля: ключ = имя свойства как в выгрузке,
рамка пятью ключами, цвет в четырёх формах. Прежние align/valign/wrap работают
и дальше, но в описании их нет — иначе у модели появляется развилка.

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

Пример из спеки скомпилирован и проверен валидатором.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:14:31 +03:00
Nick ShirokovandClaude Opus 5 3c7f7345bb fix(mxl-compile,mxl-decompile): не терять колонку, объявленную без формата
Платформа иногда перечисляет колонку в раскладке с <formatIndex>0</formatIndex> —
колонка объявлена, формата у неё нет (4% макетов корпуса). Компилятор такие
опускал, декомпилятор их не видел, и элемент терялся целиком.

Выражается пустым значением в columnStyles. Для авторинга это бесполезно —
ни одна задача не звучит как «объяви колонку без свойств», — поэтому в описании
DSL записи нет: она нужна только чтобы раундтрип не терял байты.

На пилоте потери в категории colset[].col[].formatIndex упали с 39 до 35.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:08:03 +03:00
Nick ShirokovandClaude Opus 5 4e389d0ef1 feat(mxl-compile,mxl-decompile): columnStyles — оформление колонки
У ячейки есть style, у строки rowStyle, у колонки не было ничего — при том что
колонка ссылается в ту же палитру <format> и несёт те же свойства. В корпусе
ERP колонки используют шрифт (3006 форматов), выравнивание, рамки, скрытие и
прочее; всё это терялось при раундтрипе.

Колонка получает такой же именованный стиль: columnStyles рядом с columnWidths,
ключи той же грамматики диапазонов ("1", "2-8", "5,7,9"), значение — имя из
styles. Внутри columnSets тот же ключ. Формат колонки собирается из ширины и
свойств стиля: запись в палитре одна.

Шрифт по умолчанию колонке не навязываем — формат колонки без оформления это
ровно <width>, как пишет платформа.

Попутно два дефекта декомпилятора: стиль колонки не учитывался при отсечении
неиспользуемых стилей и пропадал целиком, а набор из одних неприметных свойств
(отступ, защита) получал зарезервированное имя default и тоже терялся.

На пилоте потери в категории colset[].col[].formatIndex упали с 86 до 39.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:57:52 +03:00
Nick ShirokovandClaude Opus 5 c9e490bd25 feat(mxl-decompile): читать формат целиком, а не девять полей
Декомпилятор снимал из <format> только шрифт, четыре рамки, выравнивания,
перенос и форматную строку — остальные тридцать с лишним свойств терялись
молча. Компилятор их уже умеет, но до раундтрипа они не доезжали.

Теперь формат читается целиком по общей таблице типов — той же, что у
компилятора: рамки разворачиваются в описание линии из палитры, булевы и
целые приводятся по типу, цвет из web/win-палитры возвращается в привычную
запись (префикс узла разрешается в URI пространства имён).

Имя стиля осталось читаемой меткой по заметным свойствам: различает стили
ключ, имя лишь помогает автору ориентироваться.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:15:16 +03:00
Nick ShirokovandClaude Opus 5 63f7c7a99d test(mxl): переснять снэпшоты соседей после свёртки рамки
Снэпшоты mxl-decompile/mxl-info/mxl-validate строятся preRun-прогоном
mxl-compile. Дрейф одного вида: четыре одинаковые стороны рамки стали
одним <border>.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:34:55 +03:00
Nick ShirokovandClaude Opus 5 d3c16f56c5 feat(mxl-compile): стиль умеет все свойства формата, а не семь
Стиль описывал 7 свойств из 47, которые платформа хранит в <format>. Не было
самого частого тега корпуса — backColor (1.7 млн вхождений), а также textColor,
textPlacement, protection, hidden, indent, borderColor, textOrientation и хвоста.

Теперь ключ стиля — имя тега платформы, без исключений: помнить, какие ключи
названы по-своему, больше не нужно. Прежние align/valign/wrap продолжают
работать молча, как синонимы; там же CSS-имена (background, color,
border-bottom) и русские имена свойств. Таблицы типов значений и допустимых
значений перечислений сняты с корпуса, а не выписаны на глаз.

Рамка: пять плоских ключей (border и четыре стороны) со значением
{ style, width } либо строкой стиля; линия регистрируется в палитре <line>,
которая раньше знала только «тонкую» и «толстую» Solid. Четыре одинаковые
стороны сворачиваются в один <border> — правило проверено на корпусе:
70 265 свёрнутых форматов против 36 783 записанных по сторонам, и среди
вторых нет ни одного с четырьмя совпадающими значениями.

Цвет — нотация самой платформы: #RRGGBB, style:Имя, web:Имя, win:Имя. Первые
две пишутся дословно (префикс style объявлен в корне документа), для web и win
платформа дописывает объявление xmlns прямо на узел — делаем так же.

containsValue / valueType / controlType намеренно не заведены: это свойства
конкретной ячейки, а стиль — сущность общая, один на многие ячейки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:34:54 +03:00
Nick ShirokovandClaude Opus 5 c559696c5a refactor(mxl-compile): формат — таблица тегов вместо двенадцати полей
Формат был фиксированной записью из 12 полей, выписанной руками в четырёх
местах: ключ дедупликации, набор свойств, резолв стиля и эмиссия палитры.
Добавление свойства стоило восьми правок в двух портах, а свойств у формата
в выгрузке 47.

Теперь формат — набор «тег платформы → значение», а порядок эмиссии задаёт
один канонический список тегов. Список снят с корпуса ERP: 766 960 форматов,
ни один не нарушает эту последовательность.

Вывод не меняется: пилот из 40 макетов скомпилировался побайтово так же, как
до правки, обоими портами.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 14:45:30 +03:00
Nick ShirokovandClaude Opus 5 c5a65ef620 test(mxl): переснять снэпшоты соседних навыков после правки эмиссии
Снэпшоты mxl-decompile/mxl-info/mxl-validate строятся preRun-прогоном
mxl-compile, поэтому дрейфуют вместе с ним. Дрейф ровно двух видов:
убран избыточный <i> и схлопнуты пустые строки в indexTo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:04:59 +03:00
Nick ShirokovandClaude Opus 5 cd9a800f8b fix(mxl-compile): писать номер колонки и пустые строки как платформа
Две правки эмиссии, обе выведены из корпуса erp_8.3.24 (10924 макета,
15.2 млн ячеек) и проверены на нём же без единого контрпримера.

Номер колонки <i> платформа пишет только при разрыве последовательности:
подряд идущая ячейка его не несёт, читатель ведёт счётчик сам. Мы писали
всегда — из 1.2 млн записанных платформой номеров ни один не избыточен.

Подряд идущие одинаковые ПУСТЫЕ строки платформа хранит одним rowsItem
с indexTo; непустые не схлопывает, даже когда они совпадают (98153 таких
случая в корпусе). Мы не эмитили indexTo вовсе.

На пилоте (40 макетов) доля строк документа, совпавших с оригиналом
байт в байт, выросла с 0.3% до 16.7%; все 9 диапазонов indexTo оригинала
воспроизведены точно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:04:59 +03:00
Nick ShirokovandClaude Opus 5 3e7e105c43 fix(mxl-compile): писать ошибки в stderr напрямую, а не через Write-Error
Write-Error оборачивает сообщение в ErrorRecord: приписывает имя скрипта,
добавляет хвост CategoryInfo и ломает текст по ширине окна консоли. Длинные
сообщения приезжали разорванными посреди слова, и по ним нельзя было
ни искать подстроку, ни читать их глазами.

Остальные четырнадцать мест в файле уже писали через Console.Error —
приведены оставшиеся шесть. Тексты сообщений не менялись.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:16:42 +03:00
Nick ShirokovandClaude Opus 5 3bb717b2cc docs(mxl-compile): убрать из описания textLanguages совет и скрытую статистику
«Внутри конфигурации набор обычно одинаков» — тот же вывод из замеров, только
без числа: проверить его по месту нельзя. Совет смотреть соседние макеты толкает
на лишний обход при авторинге с нуля, а упоминание, что платформа разрешает
необъявленные языки, читается как приглашение их указывать.

Осталось свойство самого ключа: он ни на что в конфигурации не смотрит.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:09:20 +03:00
Nick ShirokovandClaude Opus 5 7b78485f42 docs(mxl-compile): убрать из спеки статистику по корпусу и внутренности компилятора
Проценты по типовым конфигурациям модель может принять за правило, а объяснение,
почему набор языков нельзя вывести из текстов, ей для применения не нужно.
Осталось то, что помогает выбрать значение: языки текстов и языки конфигурации
совпадать не обязаны, ориентир — соседние макеты той же конфигурации.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:03:49 +03:00
Nick ShirokovandClaude Opus 5 683ed8e08d feat(mxl-compile,mxl-decompile): textLanguages — текст строкой на всех языках макета
Строка в text/template разворачивается по одному элементу на каждый язык из
документного ключа textLanguages (по умолчанию только ru). Декомпилятор собирает
набор языков по всем текстам макета и пишет строкой текст, одинаковый на всём
наборе; различающийся остаётся объектом.

Языки текстов и языки, объявленные в конфигурации, — разные вещи: типовые ERP
объявляют один ru, а тексты хранят под ru и en. Набор постоянен внутри
конфигурации, поэтому он документный, а не вычисляемый: к моменту компиляции
тексты уже свёрнуты в строки.

Заодно: эмиссия tl проверяет НАЛИЧИЕ ключа text/template, а не истинность —
пустая строка это текст, платформа такие ячейки пишет с пустым tl, а по
истинности он терялся.

Проверено на пилоте (40 макетов ERP): скомпилированный XML совпадает с прошлым
прогоном байт в байт обоими рантаймами, объём JSON −47%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:00:20 +03:00
Nick ShirokovandClaude Opus 5 4cceb03269 feat(mxl-compile,mxl-decompile): позиционный список ячеек в выводе декомпилятора
Компилятор давно принимал короткую форму, а декомпилятор всегда писал самую
развёрнутую. Теперь он выбирает самую короткую из применимых, как это делает
skd-decompile.

Короткая запись стала свойством СПИСКА ЯЧЕЕК, а не строки: cells принимает
позиционный список, поэтому строка с height или rowStyle тоже может им пользоваться.
Раньше наличие свойств строки лишало её короткой записи — искусственная связка.
Если же своих свойств у строки нет, она пишется просто массивом, без ключа cells.

Позиционный список опознаётся по наличию элемента-строки или null; список из одних
объектов читается как обычный — для простой строки обе трактовки дают один результат.

Три дефекта, вскрытых проверкой на потери (каждый раз XML переставал совпадать с
прежним, и это приводило к причине):

- компилятор двигал позицию на единицу за элемент, поэтому объектный элемент со
  span: 3 занимал одну позицию вместо трёх и всё правее него уезжало влево. У маркеров
  ">" этого не было — каждый съедает свою позицию;
- проход «удалить неиспользуемые стили» не заглядывал в строки-массивы: у массива нет
  свойства cells, и стиль, использованный только внутри такой строки, вырезался как
  неиспользуемый;
- в py тот же проход падал на позиционном списке — "style" in c для None бросает
  TypeError, а для строки делает подстрочный поиск.

Плюс ловушка PowerShell: += разворачивает вложенный массив, и строка-массив
расплющивалась в список строк — нужна запятая.

Проверено: XML на всех 40 макетах пилота совпал с прежним БАЙТ В БАЙТ, то есть
сокращение без потерь. Объём декомпилированного JSON меньше на 11%. Тесты 54/54 на
обоих рантаймах, гарды зелёные.

В WORKFLOW уточнена находка про паритет портов: декомпиляторы расходятся по JSON на
33 макетах из 40, а компиляторы на одном и том же входе — всего на 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:35:58 +03:00
Nick ShirokovandClaude Opus 5 a9697b613d fix(mxl-decompile): не писать style по умолчанию, чинить потерю стиля при rowStyle
Одно условие делает две вещи.

Сокращение: у 56% ячеек (19 737 из 35 190) стоял "style": "default", хотя компилятор
и так подставляет default, когда ключа нет и у строки нет rowStyle. Чистый шум;
объём декомпилированного JSON на пилоте упал на 7%.

Починка: при ЗАДАННОМ rowStyle ячейка со стилем default, которая его не наследует,
теряла стиль вовсе — условие содержало -not rowStyleName, — и при обратной сборке
такая ячейка наследовала rowStyle вместо своего умолчания.

Проверка на потери: из 40 макетов пилота 23 собрались байт в байт как прежде. У 17
XML изменился, и это разобрано поячеечно: во всех 181 разошедшейся ячейке НИ ОДНА
версия не совпадает с оригиналом — там backColor, фон ячейки, который DSL не
поддерживает вовсе (числится в ограничениях). То есть правка переключает между двумя
одинаково неверными отображениями неподдерживаемой конструкции, не улучшая и не
ухудшая совпадение: фактов, совпавших с оригиналом, было 8183 и осталось 8183.

Проверено: тесты 54/54 на обоих рантаймах. Дрейф снэпшота один и ожидаемый — из
back.json ушёл "style": "default".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 21:36:07 +03:00
Nick ShirokovandClaude Opus 5 1e06e43e92 feat(mxl-compile,mxl-decompile): многоязычный текст ячейки
Платформа хранит текст ячейки по элементу на язык. Декомпилятор брал ПЕРВЫЙ и терял
остальное, компилятор писал всегда ru захардкоженно. В корпусе ERP текст лежит и под
ru, и под en у 10 730 макетов из 10 924 — то есть терялось у 98%.

Форма взята готовая — конвенция ML-значений из docs/meta-dsl-spec.md §4.4, по которой
уже живут synonym, tooltip и title в метаданных и формах: строка означает русский
текст, объект даёт по надписи на язык в порядке ключей. Новых понятий не вводили,
ключи те же (text и template).

Документный ключ для языков НЕ заводили, и это измерение, а не экономия: блок
languageSettings мы и так эмитим байт в байт как платформа (currentLanguage ru,
defaultLanguage ru, один languageInfo). Отклонения редки — 8 макетов ERP с
currentLanguage en, 3 без него, 157 с объявленной парой ru+en, всего около 1,6%.
Заодно это снимает развилку из разбора PR #62: список языков компилятор дописывать
не должен, платформа его не дописывает.

Эффект на пилоте: категория row[].cell[].tl упала с 47 252 до 12 488 — минус 74%,
самое крупное улучшение кампании. Остаток частично классифицирован: 348 строк — ячейки
с ПУСТЫМ <tl> у оригинала, которых мы не эмитим вовсе; полностью разобрать мешает
обрезка диффа по 400 строк на объект.

Снэпшоты не дрейфанули: кейсы пишут текст строкой, и вывод для неё прежний.

Проверено: тесты 54/54 на обоих рантаймах, verify-snapshots 24/24, вывод портов
совпадает байт в байт и в компиляции, и в декомпиляции.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:52:03 +03:00
Nick ShirokovandClaude Opus 5 572b75984a fix(mxl-compile): неоформленная ячейка ссылается на формат по умолчанию
Платформа держит для неоформленного макета ОДИН формат — ширину колонки, он же
defaultFormatIndex, и все ячейки указывают на него. Компилятор заводил каждой ячейке
собственный формат с <font>0</font>, где ноль означает «шрифт не задан», то есть
формат был пуст по смыслу и отличался от умолчания только своим существованием.

Теперь ячейка, у которой не задано ничего (шрифт умолчательный, нет рамок,
выравнивания, переноса, типа заполнения и формата числа), ссылается на
defaultFormatIndex. На минимальном макете палитра сократилась с двух форматов до
одного и совпала по форме с платформенной.

На корпусе правка закрывает случай неоформленной ячейки начисто: на макете
АтрибВыгрузкиXML2015Кв1 расхождения по формату исчезли полностью, блок диффа
сократился с 75 строк до 40, и всё оставшееся — многоязычный текст. В агрегате по
пилоту это 497 строк из 64 865: остальное держат макеты, где ячейки реально
оформлены, и там расходится состав самих форматов — отдельная категория.

Дрейф 18 снэпшотов разобран построчно, посторонних строк нет: 170 сменившихся
индексов <f>, 36 границ <format> (палитра стала короче), 18 удалённых <font>.

Проверено: тесты 53/53 на обоих рантаймах, verify-snapshots 23/23.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:20:35 +03:00
Nick ShirokovandClaude Opus 5 66c7364c37 fix(mxl-compile): выводить идентификатор раскладки как корректный UUIDv3
Идентификатор колоночной раскладки выводится из имени детерминированно — это
требование, а не оптимизация: случайный давал бы другой файл при каждой компиляции,
снэпшоты не совпадали бы сами с собой, а повторная сборка того же определения
порождала бы диф.

Но выводился он неправильно: сырой хэш форматировался как UUID, без битов версии и
варианта. В выдаваемом значении версия оказывалась 7, вариант 1 — таких не бывает,
то есть формально это был не UUID, а шестнадцатеричная строка нужной формы. Платформа
такое принимает (проверено сертификацией), но упереться в валидатор мы могли в любой
момент — ровно того же сорта риск, что сертификация уже ловила в этой ветке.

Теперь это штатный UUID версии 3 (имя + MD5, RFC 4122): для задачи «вывести
идентификатор из имени» существует именно он. Детерминированность сохранена, оба
порта дают одинаковое значение.

Проверено: тесты 53/53 на обоих рантаймах, verify-snapshots 23/23.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:07:02 +03:00
Nick ShirokovandClaude Opus 5 61199cfe9e test(mxl-compile): объяснить магический GUID в имени кейса
В ожиданиях column-sets стоит конкретный идентификатор раскладки, и по кейсу
не видно, откуда он взялся. Имя кейса теперь говорит, что id выводится из
имени детерминированно и что ожидаемое значение — md5 от имени раскладки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:00:33 +03:00
Nick ShirokovandClaude Opus 5 cbe7abdcbb fix(mxl-compile): не эмитить fillType=Text, UUID для идентификатора раскладки
Две правки по корпусу и по 1С-сертификации.

fillType=Text платформа практически не пишет: на выборке корпуса 344 981 текстовая
ячейка из 348 023 (99,1%) ссылается на формат БЕЗ fillType — наличие <tl> и так
означает текст. Parameter и Template платформа пишет, их оставляем. Правило то же,
что и везде: эмитим то, что эмитит платформа, а не то, что верно в рантайме.

Идентификатор колоночной раскладки платформа хранит как UUID и другой не принимает —
макет с <id>узкая</id> она отвергала целиком. Имя, похожее на UUID (то есть пришедшее
декомпиляцией), проходит насквозь, иначе поехал бы раундтрип; читаемое имя автора
превращается в UUID ДЕТЕРМИНИРОВАННО, из хэша имени, чтобы повторная компиляция
давала тот же файл. Оба порта дают одинаковый хэш.

Дефект с идентификатором нашла 1С-сертификация: ни юнит-кейсы, ни раундтрип по
корпусу его не видели — там идентификаторы приходят из исходных макетов и уже
являются UUID.

Дрейф 33 снэпшотов разобран построчно, посторонних строк нет: 116 сменившихся
индексов формата, 49 удалённых fillType, 43 перестановки свойств внутри палитры,
16 границ <format> (палитра стала короче).

Проверено: тесты 53/53 на обоих рантаймах, verify-snapshots 23/23.

Замечание на будущее: на корпусе эта правка снимает лишь 170 расхождений из 64 865
в категории формата ячейки. Основная причина другая — платформа даёт неоформленной
ячейке ссылку на формат по умолчанию, а компилятор заводит ей собственный формат
с <font>. Это отдельный заход, он сдвинет снэпшоты повторно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:58:35 +03:00
Nick ShirokovandClaude Opus 5 8302b17814 fix(mxl-compile): внятный отказ на объектную форму columnSet
Раскладка адресуется именем из columnSets. Инлайновой формы в этом DSL нет ни у
чего: style у ячейки и font внутри стиля тоже только имена — именованные сущности
объявляются один раз и всюду адресуются по имени. Объектная форма стала бы второй
формой одного и создала бы асимметрию «раскладку инлайнить можно, а стиль нельзя».

Объект в columnSet и раньше не портил вывод — навык падал с «Unknown 'columnSet'»,
подставляя сериализованный объект. Но сериализация у портов разная (@{columns=5}
против {'columns': 5}), то есть сообщения расходились. Теперь это отдельная проверка
с текстом, одинаковым в обоих портах и прямо называющим правило.

Кейсы: column-sets (позитивный, ссылка и объявление доезжают до XML) и
error-columnset-inline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:26:01 +03:00
Nick ShirokovandClaude Opus 5 26b38e2ee3 docs(mxl): колоночные раскладки в описании DSL
Инкремент B изменил DSL, а документация осталась от предыдущей версии.

Добавлены columnSets в карту структуры и в таблицу верхнего уровня, раздел про
колоночные раскладки со ссылкой columnSet у области, оговорка что columns: 0 —
допустимая величина (все строки живут в именованных раскладках) и что позиции
колонок проверяются по ширине раскладки своей области.

Из ограничений убран пункт про несколько наборов колонок: теперь поддержаны.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:21:36 +03:00
Nick ShirokovandClaude Opus 5 a1642cbe60 feat(mxl-compile,mxl-decompile): колоночные раскладки
Инкремент B кампании mxl-roundtrip. Табличный документ может задавать индивидуальные
ширины колонок для группы строк — в XML это несколько элементов <columns>, из которых
раскладка БЕЗ <id> является умолчанием (ровно одна в каждом макете корпуса), а остальные
адресуются GUID, на который ссылаются строки и области. Декомпилятор читал только первый
<columns>: ширины прочих раскладок терялись, а на макетах с пустым умолчанием он отдавал
columns: 0. Несколько раскладок есть у 63% макетов ERP.

DSL: документные columns/columnWidths — раскладка по умолчанию, дополнительные объявляются
в columnSets (ключ — идентификатор), область ссылается ключом columnSet. Форма повторяет
уже принятую в этом DSL пару «словарь именованных штук наверху — ссылка по имени на
потребителе», как styles/style и fonts/font.

Склейки раскладок по содержимому НЕТ, и это исправление собственной ошибки: замер, на
котором строилось прежнее решение, вырезал идентификатор через findtext('columnsID'),
тогда как у набора он называется <id> (columnsID — имя только у ссылок). Вырезание не
срабатывало, и наборы «различались» просто из-за разных GUID. Правильный замер: в 337
макетах из 517 есть наборы с полностью одинаковым содержимым, до 59 дублей в одном
макете. Содержимое раскладку не опознаёт — опознаёт идентификатор.

Попутно закрыты два дефекта, вскрытые корпусом:

- проверка обязательных полей смотрела на истинность значения, а не на наличие ключа:
  columns: 0 — осмысленная величина (умолчание пустое, все строки в именованных
  раскладках), и компилятор объявлял её отсутствующей. Тот же класс, что был с col;

- позиции колонок сверялись с документным columns, тогда как ширина сетки у каждой
  раскладки своя. Проверки диапазона col, автопотока и короткой формы переведены на
  ширину раскладки области.

На пилоте из 40 макетов ERP отказов больше нет ни одного (было 26): цикл проходят все 40
на обоих рантаймах. Скомпилированный XML портов совпадает байт в байт.

Записана находка (debug/mxl-roundtrip/WORKFLOW.md): порты mxl-decompile расходятся по
выходному JSON на 11 макетах из 12 — порядок стилей в словаре, при идентичном XML.
Не ловилось потому, что кейсы снэпшотят входной Template.xml, а не выходной JSON.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:01:47 +03:00
Nick ShirokovandClaude Opus 5 d002de3a26 docs(mxl): убрать XML-уровень и механику из описания DSL
Продолжение чистки протёкшей реализации.

- «тип заполнения: param → Parameter, text → Text, template → Template» — автор DSL
  ключ fillType не пишет и в выводе mxl-compile его не видит, это имена XML-перечисления.
  Заменено на то, что действительно нужно: содержимое задаётся одним из ключей param /
  text / template, где template — текст со вставками [Параметр]. Раньше синтаксис вставок
  в инструкции не упоминался вовсе, хотя он куда полезнее перечисления. (У mxl-info
  fillType остаётся: там навык объясняет свой собственный вывод.)

- «компилятор создаёт ячейки для ВСЕХ колонок строки» — механика. Оставлен наблюдаемый
  контракт: стиль применяется ко всей ширине строки, оттуда сплошные рамки.

- «заданное имя компилятор разворачивает в именованную область типа строки» — то же самое.
  Теперь сказано, что даёт имя автору: область доступна как ПолучитьОбласть("Имя").

- убран пункт «"|" продолжает ячейку, только если её позиция известна явно». Он ссылался
  на поведение, которого в документации нет (позиция ячеек без col выводится прощающим
  вводом), то есть документированное ограничение опиралось на недокументированную
  механику. Случай закрыт сообщением об ошибке.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 18:20:30 +03:00
Nick ShirokovandClaude Opus 5 8cae947f4b docs(mxl): убрать из спеки результаты исследования, привести правила в порядок
Продолжение чистки после перечитывания глазами модели, которая применяет навык.

- из раздела ограничений убраны доли по корпусу ERP. Это результат исследования, а не
  то, что помогает применять навык: модель работает с конкретным макетом, а не с
  популяцией, и числа стареют. Перечень того, что теряется при round-trip, остался
  списком; замеры живут в материалах кампании;

- фраза про ошибки короткой формы была вырвана из контекста: шла после списка
  ограничений, начиналась с символа в кавычках, и до самого конца было непонятно, что
  речь про отказ. Плюс в один ряд попало разнородное — три случая про сам шорткат и
  переполнение columns, которое к короткой форме не привязано. Переписано правилами:
  маркеру нужно, что продолжать; объектный элемент не несёт col; элементов не больше
  columns. Поведение при нарушении — одной фразой в конце;

- в SKILL.md ключевые правила шли вперемешку по уровням (страница, ячейка, строка,
  ячейка, область). Пересобраны сверху вниз: документ → область → строка → ячейка;

- буллет про namedAreas сокращён: обнаружимость ключа даёт карта структуры, а из
  правил там неочевидно только отсутствие ключа type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 18:16:40 +03:00
Nick ShirokovandClaude Opus 5 e268fbd71f docs(mxl): актуализировать ограничения, убрать протёкшую реализацию
Раздел ограничений врал в обе стороны. Он обещал потерю областей Columns и
Rectangle — они поддержаны предыдущим коммитом; и молчал про то, что теряется на
самом деле. Проверено по коду обоих навыков, доли — по корпусу ERP 8.3.24:
ячейки-поля ввода (53% макетов), несколько наборов колонок (63%), объединения вне
ячеек (16%), рисунки (2%), цвета, скрытые строки, отступ, посторонние стили линий,
группировки, колонтитулы и параметры печати. Отдельно записано, что многоязычные
надписи не поддерживаются: при разборе берётся первый вариант текста, при генерации
язык всегда ru — для двуязычных макетов остальные языки теряются.

Заодно убрано то, что описывает реализацию, а не использование:

- таблицы с точными текстами сообщений об ошибках и кодом возврата. Модель, вызвавшая
  навык, видит и текст, и код прямо в выводе; документировать их незачем, а протухают
  они молча — ровно как протух раздел ограничений. Правила, из-за которых ошибка
  возникает, остались; сами строки зафиксированы в кейсах через expectError, где
  расхождение ловится механически. В остальных пяти спеках таких таблиц и не было —
  там принята одна фраза «ненулевой код выхода и сообщение в stderr»;

- фраза про порядок эмиссии именованных элементов: автор DSL на него не влияет.
  Сам факт (платформа хранит их отсортированными по имени) перенесён в
  docs/1c-spreadsheet-spec.md, где описывается XML-уровень, вместе с оговоркой,
  что случай с «ё» не проверен.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 18:12:47 +03:00
Nick ShirokovandClaude Opus 5 f13fe140f0 docs(mxl-compile): дописать namedAreas в карту DSL и свести терминологию
Перечитал инструкцию глазами модели, которая приходит делать макет:

- в карте структуры отсутствовал ключ namedAreas — модель, которой нужна область
  из колонок (в печатных формах это обычное дело), из инструкции не узнала бы, что
  такое вообще выразимо. Карта верхнего уровня должна быть полной;

- в одном предложении соседствовали «область» и «блок». Платформа знает только
  «область», второй термин появился по дороге — убран из инструкции и спеки;

- пример строк-массивов висел без подводки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 18:06:57 +03:00
Nick ShirokovandClaude Opus 5 720ea05325 feat(mxl-compile,mxl-decompile): безымянные блоки и области координатами
Инкремент A кампании mxl-roundtrip. Блочная форма DSL не выражала больше половины
корпуса: у 34% макетов ERP есть строки вне именованных областей, у 21% нет ни одной
области типа Rows. Декомпилятор такие строки терял, а на макетах целиком из
Rectangle отдавал areas: [] — компилятор отвечал "Required field 'areas' is missing".
На пилоте из 40 макетов это 10 отказов из 26.

Что сделано:

- имя у блока стало необязательным. Блок без имени — просто кусок сетки; именованную
  область он не создаёт. Отдельный «плоский режим» не нужен: макет без выразимых
  блоков это один безымянный блок;

- namedAreas — именованные области координатами, для всего, что блоком не ложится
  (не-Rows и пересекающиеся). Тип области НЕ указывается: он выводится из заданных
  осей, ровно как в ТабличныйДокумент.Область() — только строки дают полосу строк,
  только колонки полосу колонок, обе оси прямоугольник. Так нельзя написать
  противоречие вроде type: Rows с колоночными координатами;

- диапазон записывается уже существующей грамматикой DSL (как ключи columnWidths):
  число или "N-M". Список через запятую запрещён — область непрерывна, платформа
  разрывную не хранит;

- прощающим вводом принимается платформенный адрес "R1C1:R2C2" и правило «0 значит 1»;
  в документацию не вынесено;

- декомпилятор перестал пропускать области не-Rows (там стоял безусловный continue) и
  режет сетку на блоки детерминированно: непересекающиеся Rows задают границы, дыры
  становятся безымянными блоками, остальное уходит в namedAreas.

Отдельно: именованные элементы теперь эмитятся отсортированными по имени. Платформа
хранит их именно так — на выборке 541 макета с несколькими элементами иного порядка
нет ни разу. Сортировка ординальная и регистронезависимая; Sort-Object по умолчанию
сортирует по текущей культуре и на кириллице дал бы другой порядок. Отсюда дрейф 13
снэпшотов — чистая перестановка, диффы симметричны, число элементов не изменилось.

На пилоте отказы areas: [] закрыты полностью (10 → 0), цикл переживают 24 макета
вместо 14. Оставшиеся 16 отказов — колоночные раскладки, это инкремент B.

Правка на ps1, зазеркалена в py; вывод портов и в компиляции, и в декомпиляции
совпадает байт в байт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 16:48:33 +03:00
Nick ShirokovandClaude Opus 5 f6a478c85c fix(mxl-decompile,mxl-compile): дробный размер шрифта
Размер шрифта в платформенных макетах бывает дробным (8.3, 6.8, 9.8, 11.3, 14.3).
Оба навыка приводили его к целому: py падал голым ValueError на int(), ps1 ТИХО
округлял через [int] — снова шумный отказ против тихой порчи, как было с col.

Найдено раундтрипом по корпусу ERP 8.3.24: на стратифицированной выборке из 40
макетов это давало 12 отказов декомпиляции — 30% выборки и ВСЕ отказы этого этапа.
После правки декомпиляция не падает ни на одном, цикл переживают 14 макетов
вместо 10.

Размер читается инвариантной культурой и остаётся целым, когда дробной части нет,
иначе "10" превратилось бы в "10.0". Эмиссия в XML тоже переведена на инвариантную
культуру: интерполяция строкой отдавала бы "8,3" под русской локалью.

Правка на ps1, зазеркалена в py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:03:15 +03:00
Nick ShirokovandClaude Opus 5 ec7e72d0a4 docs(mxl-compile): убрать из инструкции ссылку на DSL соседнего навыка
Фраза «та же форма, что у макетов в /skd-compile» ничего не даёт модели, которая
про DSL СКД не знает, и подталкивает к лишнему ту, которая знает: рядом с rows
там живут widths, minHeight, пресеты стилей и параметры с drilldown, которых в
макете табличного документа нет. Совпадают только четыре соглашения внутри строки,
а «та же форма» читается как «работает так же целиком».

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 14:11:54 +03:00
Nick ShirokovandClaude Opus 5 6719d7e647 feat(mxl-compile): короткая форма строки — массив ячеек
Строка, записанная массивом ("rows": [["А","Б","В"]]), молча превращалась в
пустую строку в обоих портах. Форма не выдумана: ровно так документированы
макеты в skd-compile, и привычка естественно переносится на табличный документ.

Масштаб потери был виден в самом репозитории — ПЯТЬ кейсов семейства mxl-*
написаны в этой форме и потому проверяли пустые макеты: снэпшот
lenient-key-case содержал единственную строку <empty>true</empty>. После правки
все пять снэпшотов наполнились ячейками, которые терялись.

Семантика повторяет skd-compile: позиция ячейки — индекс в массиве, ">"
продолжает ячейку слева (span), "|" — сверху (rowspan), null пропускает
колонку, "{Имя}" даёт параметр. Элемент можно записать и объектом, когда нужен
style или detail; col в нём запрещён — это смешение двух способов адресации.
Разворот идёт отдельным пред-проходом в обычные строки с явными col/span/rowspan,
поэтому остальной компилятор о короткой форме не знает.

Форма документирована — в отличие от прощающего пропуска col: модель пишет канон
соседнего навыка, и выравнивание двух табличных DSL убирает развилку. По каскаду
в SKILL.md только правило и пример, подробности и таблица ошибок — в спеке
(обе копии: docs/ и reference/ навыка).

Правка на ps1, зазеркалена в py: вывод портов совпадает байт в байт, decompile →
compile возвращает исходный XML байт в байт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 14:07:51 +03:00
Nick ShirokovandClaude Opus 5 ccbbecbcf2 fix(mxl-decompile): абсолютный OutputPath + общий сериализатор декомпиляторов
Безусловная склейка OutputPath с текущим каталогом давала "C:\cwd\C:\out.json" и
роняла запись сообщением про формат пути — навык не умел писать по абсолютному
пути вообще. Py-порт этот случай обрабатывал, ps1 нет. Приведено к общей идиоме
репозитория; кейс на абсолютный путь падает без правки и проходит с ней.

Кейс заодно вскрыл, что порты пишут РАЗНЫЙ JSON: ps1 отдавал стиль ConvertTo-Json
из PS 5.1 (выравнивание ключей, \uXXXX вместо кириллицы), py — json.dumps(indent=2).
Один и тот же макет давал 4708 байт против 1744. Не всплывало потому, что ни один
кейс не снимал сам JSON — все проверяли только stdout.

mxl-decompile был единственным декомпилятором мимо общего сериализатора: у
form/meta/skd-decompile для этого свой ConvertTo-CompactJson. Перенесён вариант
skd-decompile как самый полный, py дополнительно переведён на newline='' —
как у соседей. Теперь порты совпадают байт в байт, а decompile→compile даёт
исходный XML байт в байт.

Семья заведена в check-inline-drift.mjs (4 функции, 3 варианта): раньше шесть
копий жили вообще без гарда. В form-decompile переименована локальная переменная
в string-literal — копии различались только ею.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:35:50 +03:00
Nick ShirokovandClaude Opus 5 f33436adff feat(mxl-compile): прощающий ввод — ячейка без col
Ячейка без col роняла py голым KeyError, а ps1 молча брал null как 0 и писал
Col = -1 — битую ячейку без единого сообщения. При этом опустить col модели
естественно: DSL устроен как HTML-таблица (rows → cells), и одиночный заголовок
или обычная строка пишутся без позиций.

Строка, в которой col нет НИ У ОДНОЙ ячейки, теперь раскладывается слева направо
с учётом span и занятых сверху rowspan-колонок. Смешанную строку не угадываем —
это опечатка; переполнение columns и явный col вне 1..columns тоже дают внятную
ошибку в stderr вместо тихой порчи.

Документация не менялась: col остаётся единственной каноничной формой, прощающий
ввод живёт только в коде — как регистронезависимость ключей DSL. Второй способ
адресации в SKILL.md превратил бы одно правило в развилку.

Правка сделана на ps1 и зазеркалена в py; вывод портов на общем примере совпадает
байт в байт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:04:03 +03:00
Nick ShirokovandClaude Opus 5 863c7ebbff fix(skd-edit): паритет портов по тире в сообщениях
Три сообщения py-порта писали `--` там, где ps1 пишет `—`: rename-parameter
с равными именами, reorder-parameters с пустым списком и дедуп SelectedItemAuto.
Расхождение роняло кейс add-selection-auto-dedup на py-прогоне, из-за чего кейс
был ослаблен до куска строки без тире — и перестал сторожить сам текст.

Порты сведены по ps1-эталону, кейс сверяет полную строку. Сообщение про
отсутствие изменений не тронуто: оно одинаково в обоих портах, и на него
опирается отдельный кейс.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:03:43 +03:00
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
2128 changed files with 91575 additions and 21062 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 = []
+17 -2
View File
@@ -22,6 +22,21 @@ allowed-tools:
| `Version` | Версия конфигурации | | `Version` | Версия конфигурации |
| `Vendor` | Поставщик | | `Vendor` | Поставщик |
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) | | `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
| `FormatVersion` | Версия формата выгрузки (default: `2.17`) |
Оба параметра задаются от **целевой платформы** — той, на которой конфигурация будет работать, — но по
разным правилам.
`FormatVersion`**не выше** версии формата целевой платформы: 8.3.24 — `2.17`, 8.3.25 — `2.18`,
8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно: платформа читает свой формат и любой
более старый, поэтому дефолт `2.17` подходит для всей линейки 8.3.24 и выше.
`CompatibilityMode` влияет на доступную функциональность. Если в задаче не оговорено иное — **равен
целевой платформе** (`Version8_3_27` для 8.3.27); это то же самое, что «Не использовать» в
Конфигураторе, и именно такое значение платформа проставляет новой базе. Более низкий режим берут
осознанно — когда конфигурация должна работать и на старых платформах; он отключает возможности,
появившиеся позже. Выше целевой платформы — нельзя: такая конфигурация загрузится, но работать на ней
не будет.
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
@@ -36,8 +51,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name
# С версией и поставщиком # С версией и поставщиком
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2 ... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
# Другой режим совместимости # Под платформу 8.3.27 — версия формата и режим совместимости вместе
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3 ... -Name TestCfg -FormatVersion 2.20 -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
``` ```
## Верификация ## Верификация
+105 -20
View File
@@ -1,4 +1,4 @@
# cf-init v1.4 — Create empty 1C configuration scaffold # cf-init v1.14 — 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)]
@@ -9,15 +9,54 @@ param(
[string]$Vendor, [string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24", [string]$CompatibilityMode = "Version8_3_24",
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима # Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
# совместимости она не зависит: 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19, # совместимости она не зависит. Дефолт 2.17 — нижняя граница проверенного диапазона.
# 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми поддерживаемыми платформами.
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17" [string]$FormatVersion = "2.17"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# --- Format version ---
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
# на нечисловое значение: это опечатка, а не версия.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$formatRank = Get-FormatRank $FormatVersion
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
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
if ($formatRank -eq 0) {
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
exit 1
}
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
}
# «Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы, а не как DontUse:
# свежая база получает Version8_3_<своя>, и ни одна типовая в корпусе DontUse не содержит.
# Само значение легально — платформа принимает его без ошибок, — но не выживает: замерено на
# 8.3.25 и 8.3.27, выгрузка обоих возвращает Version8_3_8. Поэтому предупреждение, а не запрет.
# Сравнение регистронезависимо ЯВНО: в PS -eq таков по умолчанию, в py — нет, и молчаливое
# расхождение портов началось бы прямо здесь.
if ($CompatibilityMode -and $CompatibilityMode.ToLowerInvariant() -eq 'dontuse') {
[Console]::Error.WriteLine("WARNING: CompatibilityMode 'DontUse' is not `"no restrictions`" — the platform stores it as Version8_3_8. For no compatibility restrictions use the target platform version (e.g. Version8_3_27 for 8.3.27).")
}
# --- Resolve output dir --- # --- Resolve output dir ---
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
$OutputDir = Join-Path (Get-Location).Path $OutputDir $OutputDir = Join-Path (Get-Location).Path $OutputDir
@@ -43,6 +82,11 @@ $co6 = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString() $co7 = [guid]::NewGuid().ToString()
# --- Mobile functionalities --- # --- Mobile functionalities ---
# Версия формата как число — по ней ниже включаются вставки 2.21.
$is221 = ($formatRank -ge 221)
# TextToSpeech приехал раньше остальных вставок 8.5 — своей ступенью, поэтому гейт отдельный.
$is218 = ($formatRank -ge 218)
$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 +103,12 @@ $mobileFuncs = @(
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"), @("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false") @("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
) )
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.18 (8.3.25),
# последней в списке; в 2.21 список не менялся. Замерено выгрузками пустой ИБ шести платформ:
# 2.13/2.17 — 37 записей без неё, 2.18-2.21 — 38 с ней. Гейт обязателен и в обе стороны:
# на 2.17 тег ломает загрузку XDTO-ошибкой (проверено на 8.3.24), без тега на 2.18+ платформа
# подставит дефолт false и допишет его при выгрузке — то есть разойдётся роундтрип.
if ($is218) { $mobileFuncs += ,@("TextToSpeech","false") }
$mobileXml = "" $mobileXml = ""
foreach ($mf in $mobileFuncs) { foreach ($mf in $mobileFuncs) {
@@ -68,17 +118,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 +187,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 +198,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 +221,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 +241,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 +256,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 +316,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"
+128 -26
View File
@@ -1,19 +1,65 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-init v1.4 — Create empty 1C configuration scaffold # cf-init v1.14 — 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 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
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
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")
@@ -24,12 +70,32 @@ def main():
parser.add_argument('-Version', dest='Version', default='') parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='') parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24') parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости: # Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости.
# 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19, 8.3.27 — 2.20. # Дефолт 2.17 — нижняя граница проверенного диапазона.
# Дефолт консервативный: 2.17 читается всеми платформами. parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17')
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17', args = ci_parse_args(parser)
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
args = parser.parse_args() # Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
format_rank_value = format_rank(args.FormatVersion)
if format_rank_value == 0:
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
sys.exit(1)
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
f"but was not verified on that platform", file=sys.stderr)
# «Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы, а не как DontUse:
# свежая база получает Version8_3_<своя>, и ни одна типовая в корпусе DontUse не содержит.
# Само значение легально — платформа принимает его без ошибок, — но не выживает: замерено на
# 8.3.25 и 8.3.27, выгрузка обоих возвращает Version8_3_8. Поэтому предупреждение, а не запрет.
# Сравнение регистронезависимо ЯВНО: в PS -eq таков по умолчанию, в py — нет, и молчаливое
# расхождение портов началось бы прямо здесь.
if (args.CompatibilityMode or "").lower() == "dontuse":
print("WARNING: CompatibilityMode 'DontUse' is not \"no restrictions\" — the platform stores it as Version8_3_8. For no compatibility restrictions use the target platform version (e.g. Version8_3_27 for 8.3.27).", file=sys.stderr)
name = args.Name name = args.Name
synonym = args.Synonym if args.Synonym else name synonym = args.Synonym if args.Synonym else name
@@ -54,6 +120,11 @@ def main():
co = [new_uuid() for _ in range(7)] co = [new_uuid() for _ in range(7)]
# --- Mobile functionalities --- # --- Mobile functionalities ---
# Версия формата как число — по ней ниже включаются вставки 2.21.
is_221 = format_rank_value >= 221
# TextToSpeech приехал раньше остальных вставок 8.5 — своей ступенью, поэтому гейт отдельный.
is_218 = format_rank_value >= 218
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 +141,13 @@ def main():
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"), ("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"), ("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
] ]
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.18 (8.3.25),
# последней в списке; в 2.21 список не менялся. Замерено выгрузками пустой ИБ шести платформ:
# 2.13/2.17 — 37 записей без неё, 2.18-2.21 — 38 с ней. Гейт обязателен и в обе стороны:
# на 2.17 тег ломает загрузку XDTO-ошибкой (проверено на 8.3.24), без тега на 2.18+ платформа
# подставит дефолт false и допишет его при выгрузке — то есть разойдётся роундтрип.
if is_218:
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 +156,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 +173,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 +203,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 +219,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 +242,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 +262,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 +275,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 +324,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.7 — 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)]
@@ -89,6 +89,19 @@ $finalize = {
} }
} }
# --- Format version ---
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Reference tables --- # --- Reference tables ---
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' $guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' $identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
@@ -203,11 +216,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
} }
$version = $root.GetAttribute("version") $version = $root.GetAttribute("version")
$versionRank = Get-FormatRank $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", "2.21")) { } elseif ($versionRank -eq 0) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). Report-Error "1. Malformed version '$version' (expected N.N)"
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)" } elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} }
# Must have Configuration child # Must have Configuration child
@@ -1,10 +1,32 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-validate v1.5 — Validate 1C configuration XML structure # cf-validate v1.7 — 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',
@@ -110,6 +132,20 @@ VALID_ENUM_VALUES = {
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses' EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
# ── Format version ───────────────────────────────────────────
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
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
class Reporter: class Reporter:
def __init__(self, max_errors, detailed=False): def __init__(self, max_errors, detailed=False):
@@ -170,7 +206,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
@@ -230,11 +266,17 @@ def main():
check1_ok = False check1_ok = False
version = root.get('version', '') version = root.get('version', '')
version_rank = format_rank(version)
if not version: if not version:
r.warn('1. Missing version attribute on MetaDataObject') r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'): elif version_rank == 0:
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). r.error(f"1. Malformed version '{version}' (expected N.N)")
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)") elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
r.warn(f"1. Format version '{version}' is below the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
r.warn(f"1. Format version '{version}' is above the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
# Must have Configuration child # Must have Configuration child
cfg_node = None cfg_node = None
+9 -8
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 типа объектов конфигурации.
### Заимствование форм ### Заимствование форм
@@ -71,31 +70,33 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Заимствовать один объект # Заимствовать один объект
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
# Заимствовать форму (автоматически заимствует родительский объект) # Заимствовать форму (автоматически заимствует родительский объект)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента" ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
# Несколько объектов за раз # Несколько объектов за раз
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат" ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы) # Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
# Заимствовать форму с ВСЕМИ реквизитами объекта # Заимствовать форму с ВСЕМИ реквизитами объекта
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
``` ```
## Верификация ## Верификация
``` ```
/cfe-validate <ExtensionPath> /cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
``` ```
Конфигурацию-источник передавай и валидатору: заимствованные формы он проверяет по ней.
+509 -85
View File
@@ -1,4 +1,4 @@
# cfe-borrow v1.11 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
# 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,
@@ -16,18 +16,38 @@ function Warn([string]$msg) { Write-Host "[WARN] $msg" }
# Form data-binding tags (value = attribute path). A binding survives only if its root # Form data-binding tags (value = attribute path). A binding survives only if its root
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the # attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
# platform rejects the form with "Неверный путь к данным" on load. # platform rejects the form with "Неверный путь к данным" on load.
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath') # RowPictureDataPath тоже путь к данным («Объект.Товары.РасхождениеЗаказ», «Список.DefaultPicture»),
# а не индекс картинки: эталон Конфигуратора сохраняет его с заимствованным основным реквизитом
# и выбрасывает без него — то же правило, что у остальных путей.
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath','RowPictureDataPath')
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton. # Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath') $script:formBindingPictureTags = @('MultipleValuePictureDataPath')
# Пути ссылок параметров выбора, которые пришлось вырезать (для предупреждения в конце)
$script:droppedLinks = @()
# id основного реквизита в заимствованной форме — как у Конфигуратора
$script:mainAttrId = "1000001"
# Виды дочерних объектов, которые заимствуются в оболочку поимённо (табличные части — отдельно)
$script:childObjectKinds = @('Attribute','Dimension','Resource','AddressingAttribute')
# Прямые дети <Form>, которые в заимствованную форму не переносятся.
# Структурные секции: AutoCommandBar и ChildItems забираются отдельно, остальные выбрасываются целиком.
$script:formStructuralSections = @('Events','Attributes','Commands','Parameters','CommandInterface')
# Свойства формы, значение которых — имя реквизита формы (реквизиты не заимствуются, ссылка повиснет).
$script:formAttributeRefProps = @('ReportResult','DetailsData','VariantAppearance','GroupList')
# Strip data-binding tags whose root attribute isn't borrowed. # Strip data-binding tags whose root attribute isn't borrowed.
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest. # $mainAttrName задан (BorrowMainAttribute): оставить привязки от его имени, остальные снять.
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped. # Пусто (скелет без основного реквизита): снять все. Картиночные пути снимаются всегда.
function Strip-FormBindings { function Strip-FormBindings {
param([string]$xml, [bool]$keepObjekt) param([string]$xml, [string]$mainAttrName)
foreach ($tag in $script:formBindingDataTags) { foreach ($tag in $script:formBindingDataTags) {
if ($keepObjekt) { if ($mainAttrName) {
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '') # Оставить и «Список.Поле», и путь ровно на сам реквизит («Список» у таблицы формы)
$root = [regex]::Escape($mainAttrName)
$xml = [regex]::Replace($xml, "\s*<$tag>(?!$root(\.|<))[^<]*</$tag>", '')
} else { } else {
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '') $xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
} }
@@ -38,6 +58,119 @@ function Strip-FormBindings {
return $xml return $xml
} }
# Ссылки параметров выбора (<ChoiceParameterLinks>/<xr:Link>) — привязка особого рода: путь лежит
# в <xr:DataPath> и обычным стриппингом не снимается. Текстовое имя в расширении разрешается только
# если его корень объявлен в <Attributes> самой заимствованной формы; иначе платформа отвергает
# загрузку — «Неверный путь к полю - X». Реквизиты формы не заимствуются никогда, поэтому ссылка на
# них разрешима только через id: Конфигуратор подставляет id реквизита ИСХОДНОЙ формы (эталоны
# Issue66Example4/5/6, JR2433, JR2976, JR49904 — совпадение на шести расширениях). Именно id
# исходной, а не заимствованной: при заимствовании реквизиты перенумеровываются в 1000000+, а
# ссылка продолжает указывать в нумерацию базовой формы.
# Путь на основной реквизит («Объект.X») при заимствованном основном реквизите разрешается текстом
# и остаётся читаемым; без заимствования переводится в «<id>/0:<uuid реквизита объекта>».
# Реквизит, которого в источнике нет, недоступен и по uuid: такую связь вырезаем целиком.
# Пути вида «Items.<Элемент>.CurrentData.<Поле>» не трогаем — их кодировка отдельная.
function Rewrite-ChoiceParameterLinks {
param([string]$xml, $attrUuids, $formAttrIds, [string]$mainAttrName, [bool]$mainAttrBorrowed)
if ($xml -notmatch '<ChoiceParameterLinks>') { return $xml }
$mainPat = if ($mainAttrName) { [regex]::Escape($mainAttrName) } else { $null }
$mainId = if ($mainAttrName -and $formAttrIds.ContainsKey($mainAttrName)) { $formAttrIds[$mainAttrName] } else { "1" }
$xml = [regex]::Replace($xml, '(?s)\s*<xr:Link>.*?</xr:Link>', {
param($m)
$link = $m.Value
$dp = [regex]::Match($link, '<xr:DataPath[^>]*>([^<]+)</xr:DataPath>')
if (-not $dp.Success) { return $link }
$path = $dp.Groups[1].Value
# Путь на основной реквизит формы
if ($mainPat -and $path -match "^${mainPat}\.(.+)$") {
$attrName = $Matches[1]
if ($mainAttrBorrowed) {
# Реквизит объекта разрешается текстом и остаётся читаемым. Стандартное поле
# («Объект.Owner», «Объект.Date») — нет: платформа отвергает «Неверный путь к данным».
# Конфигуратор в этом случае оставляет ссылку на сам реквизит (эталон Issue66Example7_1).
if ($attrUuids.ContainsKey($attrName)) { return $link }
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}${mainId}`${2}")
}
if ($attrUuids.ContainsKey($attrName)) {
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}${mainId}/0:$($attrUuids[$attrName])`${2}")
}
return ''
}
# Односегментный путь на реквизит формы — только по id исходной формы
if ($path -notmatch '\.' -and $formAttrIds.ContainsKey($path)) {
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}$($formAttrIds[$path])`${2}")
}
# Уже непрозрачный путь (форма-источник сама из расширения) — не трогаем
if ($path -match '^\d') { return $link }
# С заимствованным основным реквизитом текстовый путь разрешается: элементы формы на месте,
# а их данные доступны через основной реквизит. Конфигуратор такие пути и оставляет текстом
# (эталон Issue66Example7_1: «Items.Товары.CurrentData.Характеристика» перенесён как есть).
if ($mainAttrBorrowed) { return $link }
# Прочее текстом не разрешается: платформа отвергает загрузку «Неверный путь к полю».
# Сюда попадают «Items.<Элемент>.CurrentData.<Поле>» — их кодировка непрозрачна и по
# имеющимся эталонам не воспроизводима. Связь параметров выбора — удобство подбора, а не
# данные: без неё форма заимствуется и работает, с ней — не грузится вовсе.
$script:droppedLinks += $path
return ''
})
# Опустевший контейнер платформе не нужен
$xml = [regex]::Replace($xml, '(?s)\s*<ChoiceParameterLinks>\s*</ChoiceParameterLinks>', '')
return $xml
}
# Имена ПРЯМЫХ детей собственного <ChildObjects> объекта — для дедупа при повторном
# заимствовании. Текстом это не снять: regex «первый <ChildObjects> до первого </ChildObjects>»
# у объекта с табличными частями обрывается на закрытии первой ТЧ, забирает имена её колонок и
# теряет то, что идёт после неё.
function Get-OwnChildObjectNames {
param([string]$objFile)
$names = @{}
if (-not (Test-Path -LiteralPath $objFile)) { return $names }
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
try { $doc.Load($objFile) } catch { return $names }
$objEl = $null
foreach ($c in $doc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
}
if (-not $objEl) { return $names }
$childObjs = $objEl.SelectSingleNode("*[local-name()='ChildObjects']")
if (-not $childObjs) { return $names }
foreach ($child in $childObjs.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$nameNode = $child.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
if ($nameNode) { $names[$nameNode.InnerText.Trim()] = $true }
}
return $names
}
# Вставка в СОБСТВЕННЫЙ <ChildObjects> объекта. Свой контейнер закрывается в файле последним:
# объект в файле один, а вложенные <ChildObjects> табличных частей закрываются раньше. Замена по
# всем вхождениям раскидывала реквизиты по каждой ТЧ — ps1 рвал XML, py прятал ТЧ внутрь ТЧ.
function Insert-IntoOwnChildObjects {
param([string]$text, [string]$content)
$closeIdx = $text.LastIndexOf('</ChildObjects>')
if ($closeIdx -ge 0) {
return $text.Substring(0, $closeIdx) + "${content}`r`n`t`t" + $text.Substring($closeIdx)
}
# Своего закрывающего тега нет — значит контейнер самозакрытый (детей у него нет, вложенных тоже)
$selfMatches = [regex]::Matches($text, '<ChildObjects\s*/>')
if ($selfMatches.Count -eq 0) { return $text }
$m = $selfMatches[$selfMatches.Count - 1]
return $text.Substring(0, $m.Index) + "<ChildObjects>${content}`r`n`t`t</ChildObjects>" + $text.Substring($m.Index + $m.Length)
}
# --- 1. Resolve paths --- # --- 1. Resolve paths ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
@@ -125,7 +258,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 +286,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",
@@ -209,7 +342,8 @@ $script:generatedTypes = @{
@{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" } @{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" }
) )
"AccountingRegister" = @( "AccountingRegister" = @(
@{ prefix = "AccountingRegisterRecord"; category = "Record" } @{ prefix = "AccountingRegisterRecord"; category = "Record" }
@{ prefix = "AccountingRegisterExtDimensions"; category = "ExtDimensions" }
@{ prefix = "AccountingRegisterManager"; category = "Manager" } @{ prefix = "AccountingRegisterManager"; category = "Manager" }
@{ prefix = "AccountingRegisterSelection"; category = "Selection" } @{ prefix = "AccountingRegisterSelection"; category = "Selection" }
@{ prefix = "AccountingRegisterList"; category = "List" } @{ prefix = "AccountingRegisterList"; category = "List" }
@@ -223,6 +357,7 @@ $script:generatedTypes = @{
@{ prefix = "CalculationRegisterList"; category = "List" } @{ prefix = "CalculationRegisterList"; category = "List" }
@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" } @{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" } @{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
@{ prefix = "RecalculationsManager"; category = "Recalcs" }
) )
"ChartOfAccounts" = @( "ChartOfAccounts" = @(
@{ prefix = "ChartOfAccountsObject"; category = "Object" } @{ prefix = "ChartOfAccountsObject"; category = "Object" }
@@ -230,12 +365,15 @@ $script:generatedTypes = @{
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" } @{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
@{ prefix = "ChartOfAccountsList"; category = "List" } @{ prefix = "ChartOfAccountsList"; category = "List" }
@{ prefix = "ChartOfAccountsManager"; category = "Manager" } @{ prefix = "ChartOfAccountsManager"; category = "Manager" }
@{ prefix = "ChartOfAccountsExtDimensionTypes"; category = "ExtDimensionTypes" }
@{ prefix = "ChartOfAccountsExtDimensionTypesRow"; category = "ExtDimensionTypesRow" }
) )
"ChartOfCharacteristicTypes" = @( "ChartOfCharacteristicTypes" = @(
@{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" } @{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" }
@{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" } @{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" }
@{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" } @{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" }
@{ prefix = "ChartOfCharacteristicTypesList"; category = "List" } @{ prefix = "ChartOfCharacteristicTypesList"; category = "List" }
@{ prefix = "Characteristic"; category = "Characteristic" }
@{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" } @{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" }
) )
"ChartOfCalculationTypes" = @( "ChartOfCalculationTypes" = @(
@@ -245,8 +383,11 @@ $script:generatedTypes = @{
@{ prefix = "ChartOfCalculationTypesList"; category = "List" } @{ prefix = "ChartOfCalculationTypesList"; category = "List" }
@{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" } @{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" }
@{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" } @{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" }
@{ prefix = "DisplacingCalculationTypesRow"; category = "DisplacingCalculationTypesRow" }
@{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" } @{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" }
@{ prefix = "BaseCalculationTypesRow"; category = "BaseCalculationTypesRow" }
@{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" } @{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" }
@{ prefix = "LeadingCalculationTypesRow"; category = "LeadingCalculationTypesRow" }
) )
"BusinessProcess" = @( "BusinessProcess" = @(
@{ prefix = "BusinessProcessObject"; category = "Object" } @{ prefix = "BusinessProcessObject"; category = "Object" }
@@ -254,6 +395,7 @@ $script:generatedTypes = @{
@{ prefix = "BusinessProcessSelection"; category = "Selection" } @{ prefix = "BusinessProcessSelection"; category = "Selection" }
@{ prefix = "BusinessProcessList"; category = "List" } @{ prefix = "BusinessProcessList"; category = "List" }
@{ prefix = "BusinessProcessManager"; category = "Manager" } @{ prefix = "BusinessProcessManager"; category = "Manager" }
@{ prefix = "BusinessProcessRoutePointRef"; category = "RoutePointRef" }
) )
"Task" = @( "Task" = @(
@{ prefix = "TaskObject"; category = "Object" } @{ prefix = "TaskObject"; category = "Object" }
@@ -320,6 +462,16 @@ $typesWithChildObjects = @(
# CommonModule properties to copy from source # CommonModule properties to copy from source
$commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall") $commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall")
# Свойства объекта, от которых зависит существование стандартного поля: без них платформа
# отвергает загрузку — «Неверный путь к данным». Конфигуратор переносит ровно их (эталоны
# Issue66Example7_1 и Issue66Example2). Проверено сплошным прогоном по типам: у регистра сведений
# без InformationRegisterPeriodicity не разрешается «Запись.Period».
$script:typeGateProps = @{
"InformationRegister" = @("InformationRegisterPeriodicity","WriteMode")
}
# Владельцы справочника — список <xr:Item>, а не скаляр: переносится фрагментом, как __TypeXml
$script:typesWithOwners = @("Catalog","ChartOfCharacteristicTypes")
# Standard system fields to skip when collecting DataPath references # Standard system fields to skip when collecting DataPath references
$script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName") $script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName")
@@ -368,6 +520,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 +548,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(";;")) {
@@ -418,6 +592,37 @@ if ($BorrowMainAttribute) {
} }
# --- 10. Helper: read source object XML --- # --- 10. Helper: read source object XML ---
# Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
# параметров выбора (см. Rewrite-ChoiceParameterLinks).
function Get-SourceAttributeUuids {
param([string]$typeName, [string]$objName)
$result = @{}
$dirName = $childTypeDirMap[$typeName]
if (-not $dirName) { return $result }
$srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml"
if (-not (Test-Path $srcFile)) { return $result }
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
$doc.Load($srcFile)
$objEl = $null
foreach ($c in $doc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
}
if (-not $objEl) { return $result }
$childObjects = $objEl.SelectSingleNode("*[local-name()='ChildObjects']")
if (-not $childObjects) { return $result }
foreach ($child in $childObjects.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
if ($child.LocalName -notin @('Attribute','TabularSection')) { continue }
$uuid = $child.GetAttribute("uuid")
$nameNode = $child.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
if ($uuid -and $nameNode) { $result[$nameNode.InnerText.Trim()] = $uuid }
}
return $result
}
function Read-SourceObject { function Read-SourceObject {
param([string]$typeName, [string]$objName) param([string]$typeName, [string]$objName)
@@ -477,6 +682,19 @@ function Read-SourceObject {
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '') $srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
} }
} }
# Владельцы: стандартное поле «Owner» появляется у справочника, только если задан Owners
if ($script:typesWithOwners -ccontains $typeName) {
$ownersNode = $propsNode.SelectSingleNode("md:Owners", $srcNs)
if ($ownersNode -and $ownersNode.HasChildNodes) {
$srcProps["__OwnersXml"] = [regex]::Replace($ownersNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
}
}
# Скалярные свойства, включающие стандартные поля своего типа
foreach ($gp in @($script:typeGateProps[$typeName])) {
if (-not $gp) { continue }
$gpNode = $propsNode.SelectSingleNode("md:${gp}", $srcNs)
if ($gpNode) { $srcProps[$gp] = $gpNode.InnerText.Trim() }
}
} }
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth # Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
@@ -597,30 +815,56 @@ function Borrow-Form {
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version. # (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
$formVersion = $script:formatVersion $formVersion = $script:formatVersion
# Find direct children: form properties, AutoCommandBar, ChildItems # Find direct children: form properties, AutoCommandBar, ChildItems.
# Секции формы отбираются по имени, а не по позиции: свойства лежат и до, и после <CommandSet>
# (корпусная проверка: у всех 794 форм документов ERP с CommandSet он стоит раньше AutoCommandBar,
# а AutoTime/UsePostingMode/RepostOnWrite — после него). Позиционная отсечка теряла весь хвост,
# и платформа молча подставляла дефолты вместо потерянных свойств.
$srcAutoCmd = $null $srcAutoCmd = $null
$srcChildItems = $null $srcChildItems = $null
$formProps = @() $formProps = @()
$reachedVisual = $false
foreach ($fc in $srcFormEl.ChildNodes) { foreach ($fc in $srcFormEl.ChildNodes) {
if ($fc.NodeType -ne 'Element') { continue } if ($fc.NodeType -ne 'Element') { continue }
if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) {
$reachedVisual = $true; $srcAutoCmd = $fc; continue $srcAutoCmd = $fc; continue
} }
if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) {
$reachedVisual = $true; $srcChildItems = $fc; continue $srcChildItems = $fc; continue
}
if ($fc.LocalName -eq 'Events' -or $fc.LocalName -eq 'Attributes' -or $fc.LocalName -eq 'Commands' -or $fc.LocalName -eq 'Parameters' -or $fc.LocalName -eq 'CommandSet') {
$reachedVisual = $true; continue
}
if (-not $reachedVisual) {
$formProps += $fc.OuterXml
} }
# Структурные секции: в расширении их содержимое недействительно (обработчики, команды и
# параметры базовой формы, ссылки командного интерфейса на команды базовой конфигурации).
if ($script:formStructuralSections -ccontains $fc.LocalName) { continue }
# Свойства, значение которых — имя реквизита формы. Реквизиты в заимствованную форму не
# переносятся, поэтому Конфигуратор такие свойства выбрасывает (проверено на форме отчёта:
# ReportResult и DetailsData выброшены, CustomSettingsFolder — имя элемента — сохранён).
if ($script:formAttributeRefProps -ccontains $fc.LocalName) { continue }
$formProps += $fc.OuterXml
} }
# Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>) # Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>)
$nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"'
# Основной реквизит исходной формы: его имя — корень путей к данным, которые нужно сохранить
# («Объект.» у формы объекта, «Список.» у формы списка, «Запись.» у формы записи регистра)
# Имя основного реквизита источника нужно в обоих режимах: по нему опознаётся корень путей
# в ссылках параметров выбора. А $mainAttrName управляет вырезанием привязок и потому остаётся
# пустым в скелетном режиме — там привязки снимаются все.
$srcMainInfo = Get-MainAttributeInfo $srcFormEl $nsStripPattern
$srcMainAttrName = if ($srcMainInfo) { $srcMainInfo.Name } else { "" }
$formAttrIds = Get-FormAttributeIds $srcFormEl
$mainAttrInfo = if ($BorrowMainAttr) { $srcMainInfo } else { $null }
$mainAttrName = if ($mainAttrInfo) { $mainAttrInfo.Name } else { "" }
if ($BorrowMainAttr -and -not $mainAttrInfo) {
Warn " У формы нет основного реквизита — -BorrowMainAttribute проигнорирован"
}
# uuid реквизитов объекта нужны ровно там, где основной реквизит НЕ попал в форму:
# только тогда путь «<основной>.X» переводится в непрозрачный вид
# Имена реквизитов объекта нужны в обоих режимах: без заимствования — чтобы построить
# непрозрачный путь, с заимствованием — чтобы отличить реквизит (разрешается текстом) от
# стандартного поля (не разрешается)
$srcAttrUuids = Get-SourceAttributeUuids $typeName $objName
# AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false # AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false
$autoCmdXml = "" $autoCmdXml = ""
if ($srcAutoCmd) { if ($srcAutoCmd) {
@@ -628,10 +872,13 @@ function Borrow-Form {
$autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '') $autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '')
$autoCmdXml = [regex]::Replace($autoCmdXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>') $autoCmdXml = [regex]::Replace($autoCmdXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>' $autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
# Strip ExcludedCommand (references to standard commands invalid in extension) # Вложенный CommandSet выбрасывается целиком, а не опустошается: Конфигуратор в заимствованной
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '') # форме оставляет только корневой (тот идёт свойством формы, здесь его нет).
$autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*<CommandSet>.*?</CommandSet>', '')
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<CommandSet/>', '')
# Strip data-binding tags whose root attribute isn't borrowed # Strip data-binding tags whose root attribute isn't borrowed
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr) $autoCmdXml = Strip-FormBindings $autoCmdXml $mainAttrName
$autoCmdXml = Rewrite-ChoiceParameterLinks $autoCmdXml $srcAttrUuids $formAttrIds $srcMainAttrName ([bool]$mainAttrInfo)
} }
# ChildItems: copy full tree, clean up base-config references # ChildItems: copy full tree, clean up base-config references
@@ -643,9 +890,11 @@ function Borrow-Form {
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>') $childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
# Strip data-binding tags whose root attribute isn't borrowed # Strip data-binding tags whose root attribute isn't borrowed
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*) # (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr) $childItemsXml = Strip-FormBindings $childItemsXml $mainAttrName
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) $childItemsXml = Rewrite-ChoiceParameterLinks $childItemsXml $srcAttrUuids $formAttrIds $srcMainAttrName ([bool]$mainAttrInfo)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '') # Вложенные CommandSet (у таблиц, полей табличного документа и т.п.) — целиком, см. выше
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<CommandSet>.*?</CommandSet>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<CommandSet/>', '')
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID) # Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '') $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '')
# Strip element-level Events (base form handlers not in extension) # Strip element-level Events (base form handlers not in extension)
@@ -824,11 +1073,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
@@ -851,17 +1111,9 @@ function Borrow-Form {
$formXmlSb.Append("`r`n") | Out-Null $formXmlSb.Append("`r`n") | Out-Null
} }
# Attributes: empty or with MainAttribute when BorrowMainAttr # Attributes: empty or with MainAttribute when BorrowMainAttr
if ($BorrowMainAttr) { if ($BorrowMainAttr -and $mainAttrInfo) {
$objTypePrefix = ""
$gtList = $script:generatedTypes[$typeName]
if ($gtList) { foreach ($g in $gtList) { if ($g.category -eq "Object") { $objTypePrefix = $g.prefix; break } } }
$mainAttrType = "cfg:${objTypePrefix}.${objName}"
$formXmlSb.Append("`t<Attributes>`r`n") | Out-Null $formXmlSb.Append("`t<Attributes>`r`n") | Out-Null
$formXmlSb.Append("`t`t<Attribute name=`"Объект`" id=`"1000001`">`r`n") | Out-Null $formXmlSb.Append("`t`t$($mainAttrInfo.Xml)`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
$formXmlSb.Append("`t`t</Attribute>`r`n") | Out-Null
$formXmlSb.Append("`t</Attributes>") | Out-Null $formXmlSb.Append("`t</Attributes>") | Out-Null
} else { } else {
$formXmlSb.Append("`t<Attributes/>") | Out-Null $formXmlSb.Append("`t<Attributes/>") | Out-Null
@@ -895,13 +1147,15 @@ function Borrow-Form {
} }
# BaseForm Attributes: same as main section # BaseForm Attributes: same as main section
if ($BorrowMainAttr) { if ($BorrowMainAttr -and $mainAttrInfo) {
$formXmlSb.Append("`t`t<Attributes>`r`n") | Out-Null $formXmlSb.Append("`t`t<Attributes>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<Attribute name=`"Объект`" id=`"1000001`">`r`n") | Out-Null # В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems
$formXmlSb.Append("`t`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null $maLines = $mainAttrInfo.Xml -split "`r?`n"
$formXmlSb.Append("`t`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null for ($li = 0; $li -lt $maLines.Count; $li++) {
$formXmlSb.Append("`t`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null if ($li -eq 0) { $formXmlSb.Append("`t`t`t$($maLines[$li])") | Out-Null }
$formXmlSb.Append("`t`t`t</Attribute>`r`n") | Out-Null else { $formXmlSb.Append("`t$($maLines[$li])") | Out-Null }
$formXmlSb.Append("`r`n") | Out-Null
}
$formXmlSb.Append("`t`t</Attributes>") | Out-Null $formXmlSb.Append("`t`t</Attributes>") | Out-Null
} else { } else {
$formXmlSb.Append("`t`t<Attributes/>") | Out-Null $formXmlSb.Append("`t`t<Attributes/>") | Out-Null
@@ -917,8 +1171,21 @@ 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"
if ($script:droppedLinks.Count -gt 0) {
$uniq = @($script:droppedLinks | Sort-Object -Unique)
Warn " Вырезано связей параметров выбора: $($uniq.Count) — путь не разрешается в расширении: $($uniq -join ', ')"
$script:droppedLinks = @()
}
# 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
# not clobber user code added to the form module). # not clobber user code added to the form module).
@@ -1023,8 +1290,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"
} }
@@ -1071,8 +1346,46 @@ function Build-InternalInfoXml {
} }
# --- 11b. Collect DataPath references from source Form.xml --- # --- 11b. Collect DataPath references from source Form.xml ---
# --- 11b1. Основной реквизит исходной формы ---
# Переносится ЦЕЛИКОМ, а не собирается из констант: имя, тип и состав детей зависят от вида формы.
# У формы объекта это «Объект»/<Тип>Object + SavedData/UseAlways/Columns, у формы списка —
# «Список»/DynamicList + Settings, у формы записи регистра — «Запись»/RecordManager + SavedData.
# Синтез фиксированного набора давал для необъектных форм «Исключение XDTO» при загрузке.
# Конфигуратор меняет у скопированного реквизита только id (эталоны Issue64UtB, Issue66Example2).
# Имена реквизитов ИСХОДНОЙ формы → их id. Ссылки параметров выбора адресуют реквизит формы
# именно по id базовой формы (см. Rewrite-ChoiceParameterLinks).
function Get-FormAttributeIds {
param($formEl)
$result = @{}
$attrs = $formEl.SelectSingleNode("*[local-name()='Attributes']")
if (-not $attrs) { return $result }
foreach ($a in $attrs.ChildNodes) {
if ($a.NodeType -ne 'Element' -or $a.LocalName -ne 'Attribute') { continue }
$nm = $a.GetAttribute("name"); $id = $a.GetAttribute("id")
if ($nm -and $id) { $result[$nm] = $id }
}
return $result
}
function Get-MainAttributeInfo {
param($formEl, [string]$nsStripPattern)
$mainAttr = $formEl.SelectSingleNode("*[local-name()='Attributes']/*[local-name()='Attribute'][*[local-name()='MainAttribute']='true']")
if (-not $mainAttr) { return $null }
$xml = [regex]::Replace($mainAttr.OuterXml, $nsStripPattern, '')
# id заменяется только в открывающем теге самого реквизита — у вложенных элементов свои
$xml = [regex]::Replace($xml, '^(<Attribute\s[^>]*?)id="[^"]*"', "`${1}id=`"$script:mainAttrId`"")
return @{ Name = $mainAttr.GetAttribute("name"); Xml = $xml }
}
function Collect-FormDataPaths { function Collect-FormDataPaths {
param([string]$formXmlPath) param([string]$formXmlPath, [string]$mainAttrName)
# Корень путей — имя основного реквизита формы: «Объект» у формы объекта, «Список» у формы
# списка, «Запись» у формы записи регистра. Зашитый «Объект» не находил ничего у необъектных
# форм, и в оболочку не заимствовалось ни одного дочернего объекта.
$root = [regex]::Escape($mainAttrName)
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
$content = [System.IO.File]::ReadAllText($formXmlPath, $enc) $content = [System.IO.File]::ReadAllText($formXmlPath, $enc)
@@ -1083,7 +1396,7 @@ function Collect-FormDataPaths {
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*) # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
# for Объект.* references — picture-path tags carry picture indices, not data attributes. # for Объект.* references — picture-path tags carry picture indices, not data attributes.
foreach ($tag in $script:formBindingDataTags) { foreach ($tag in $script:formBindingDataTags) {
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>") $bms = [regex]::Matches($content, "<$tag>[^<]*\b$root\.(\w+(?:\.\w+)*)</$tag>")
foreach ($m in $bms) { foreach ($m in $bms) {
$path = $m.Groups[1].Value $path = $m.Groups[1].Value
$segments = $path.Split(".") $segments = $path.Split(".")
@@ -1101,7 +1414,7 @@ function Collect-FormDataPaths {
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance # Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too. # fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>") $fieldMatches = [regex]::Matches($content, "<Field>[^<]*\b$root\.(\w+(?:\.\w+)*)</Field>")
foreach ($m in $fieldMatches) { foreach ($m in $fieldMatches) {
$path = $m.Groups[1].Value $path = $m.Groups[1].Value
$segments = $path.Split(".") $segments = $path.Split(".")
@@ -1115,6 +1428,30 @@ function Collect-FormDataPaths {
} }
} }
# Also scan <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в
# самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться,
# и без её заимствования платформа отвергает форму: «Неверный путь к данным».
$acMatches = [regex]::Matches($content, "<AdditionalColumns table=`"$root\.(\w+)`"")
foreach ($m in $acMatches) {
$seg0 = $m.Groups[1].Value
if ($script:standardFields -contains $seg0) { continue }
$firstLevel[$seg0] = $true
}
# Текст запроса динамического списка — такое же место ссылки на реквизиты объекта, как DataPath.
# Конфигуратор заимствует всё, что упомянуто в запросе: на эталоне Issue66Example2 это 21 из 27
# дочерних объектов, совпадение с ним точное в обе стороны. У списка без ручного запроса
# (<QueryText> нет) заимствуется только видимое на форме — эталон Issue66Example3.
# Разбирать язык запросов не нужно: имена-кандидаты отфильтрует Resolve-SourceAttributes по
# реальному составу объекта, поэтому лишние слова из запроса безвредны.
foreach ($qm in [regex]::Matches($content, '(?s)<QueryText>(.*?)</QueryText>')) {
foreach ($w in [regex]::Matches($qm.Groups[1].Value, '[\w]+')) {
$word = $w.Value
if ($script:standardFields -contains $word) { continue }
$firstLevel[$word] = $true
}
}
# Deduplicate deep paths # Deduplicate deep paths
$seen = @{} $seen = @{}
$uniqueDeep = @() $uniqueDeep = @()
@@ -1165,7 +1502,11 @@ function Resolve-SourceAttributes {
foreach ($child in $childObjs.ChildNodes) { foreach ($child in $childObjs.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue } if ($child.NodeType -ne 'Element') { continue }
if ($child.LocalName -eq 'Attribute') { # Реквизит объекта, измерение и ресурс регистра — один и тот же вид дочернего объекта с
# точки зрения заимствования, различается только имя элемента. Конфигуратор переносит их
# своим видом (эталон Issue66Example2: у регистра <Dimension> x3 и <Resource>), поэтому вид
# запоминается и выпускается как есть — иначе измерение уехало бы в файл как <Attribute>.
if ($script:childObjectKinds -ccontains $child.LocalName) {
$nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs)
if (-not $nameNode) { continue } if (-not $nameNode) { continue }
$attrName = $nameNode.InnerText $attrName = $nameNode.InnerText
@@ -1177,7 +1518,7 @@ function Resolve-SourceAttributes {
# Strip namespace declarations from Type # Strip namespace declarations from Type
$typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') $typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
$attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml } $attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml; Kind = $child.LocalName }
} }
elseif ($child.LocalName -eq 'TabularSection') { elseif ($child.LocalName -eq 'TabularSection') {
$nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs)
@@ -1227,8 +1568,16 @@ function Resolve-SourceAttributes {
$extraProps = [ordered]@{} $extraProps = [ordered]@{}
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
if ($propsNode) { if ($propsNode) {
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength", # NumberPeriodicity сюда НЕ входит: платформа считает его модификацией настроек нумерации и
"NumberType","NumberLength","NumberAllowedLength","NumberPeriodicity") # тогда требует объявить ещё и <Numerator/>, иначе /UpdateDBCfg падает — «отключать
# контролируемость свойства "Нумератор" недопустимо». Конфигуратор его не переносит
# (эталон заимствования документа: NumberType/NumberLength/NumberAllowedLength и всё).
# Загрузку это не ломает, ошибка вылезает только на обновлении конфигурации БД.
# FoldersOnTop сюда НЕ входит: платформа его у заимствованной оболочки не хранит — при
# загрузке молча выбрасывает (проверено раундтрипом: записали, выгрузили обратно, свойства
# нет). Конфигуратор его тоже не переносит. Остальные из списка сохраняются.
$propsToExtract = @("Hierarchical","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
"NumberType","NumberLength","NumberAllowedLength")
foreach ($pName in $propsToExtract) { foreach ($pName in $propsToExtract) {
$pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs) $pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs)
if ($pNode) { $extraProps[$pName] = $pNode.InnerText } if ($pNode) { $extraProps[$pName] = $pNode.InnerText }
@@ -1240,11 +1589,11 @@ function Resolve-SourceAttributes {
# --- 11d. Build adopted attribute XML --- # --- 11d. Build adopted attribute XML ---
function Build-AdoptedAttributeXml { function Build-AdoptedAttributeXml {
param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent) param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent, [string]$kind = "Attribute")
$newUuid = [guid]::NewGuid().ToString() $newUuid = [guid]::NewGuid().ToString()
$sb = New-Object System.Text.StringBuilder $sb = New-Object System.Text.StringBuilder
$sb.AppendLine("${indent}<Attribute uuid=`"${newUuid}`">") | Out-Null $sb.AppendLine("${indent}<${kind} uuid=`"${newUuid}`">") | Out-Null
$sb.AppendLine("${indent}`t<InternalInfo/>") | Out-Null $sb.AppendLine("${indent}`t<InternalInfo/>") | Out-Null
$sb.AppendLine("${indent}`t<Properties>") | Out-Null $sb.AppendLine("${indent}`t<Properties>") | Out-Null
$sb.AppendLine("${indent}`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null $sb.AppendLine("${indent}`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null
@@ -1253,7 +1602,7 @@ function Build-AdoptedAttributeXml {
$sb.AppendLine("${indent}`t`t<ExtendedConfigurationObject>${sourceUuid}</ExtendedConfigurationObject>") | Out-Null $sb.AppendLine("${indent}`t`t<ExtendedConfigurationObject>${sourceUuid}</ExtendedConfigurationObject>") | Out-Null
$sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null $sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null
$sb.AppendLine("${indent}`t</Properties>") | Out-Null $sb.AppendLine("${indent}`t</Properties>") | Out-Null
$sb.Append("${indent}</Attribute>") | Out-Null $sb.Append("${indent}</${kind}>") | Out-Null
return $sb.ToString() return $sb.ToString()
} }
@@ -1376,14 +1725,6 @@ function Merge-AttributesIntoObject {
$added = 0 $added = 0
foreach ($attr in $attrsToAdd) { foreach ($attr in $attrsToAdd) {
if ($existingNames.ContainsKey($attr.Name)) { continue } if ($existingNames.ContainsKey($attr.Name)) { continue }
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
# Expand self-closing ChildObjects if needed
if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) {
$closeWs = $objDoc.CreateWhitespace("`r`n`t`t")
$childObjs.AppendChild($closeWs) | Out-Null
}
$added++ $added++
} }
@@ -1392,7 +1733,8 @@ function Merge-AttributesIntoObject {
$allAttrXml = "" $allAttrXml = ""
foreach ($attr in $attrsToAdd) { foreach ($attr in $attrsToAdd) {
if ($existingNames.ContainsKey($attr.Name)) { continue } if ($existingNames.ContainsKey($attr.Name)) { continue }
$allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t") $kind = if ($attr.Kind) { $attr.Kind } else { "Attribute" }
$allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $kind)
} }
# Save via text manipulation to avoid namespace issues with InnerXml # Save via text manipulation to avoid namespace issues with InnerXml
@@ -1410,10 +1752,21 @@ function Merge-AttributesIntoObject {
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) } if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"') $text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Insert attributes before </ChildObjects> # Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
$text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>" # лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
# Пустой элемент: 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"
} }
@@ -1435,7 +1788,16 @@ function Borrow-MainAttribute {
Write-Error "Source Form.xml not found: $srcFormXmlPath" Write-Error "Source Form.xml not found: $srcFormXmlPath"
exit 1 exit 1
} }
$dp = Collect-FormDataPaths $srcFormXmlPath # Имя основного реквизита исходной формы — корень путей, которые надо собрать
$dpDoc = New-Object System.Xml.XmlDocument
$dpDoc.PreserveWhitespace = $true
$dpDoc.Load($srcFormXmlPath)
$dpInfo = Get-MainAttributeInfo $dpDoc.DocumentElement '\s+xmlns(?::\w+)?="[^"]*"'
if (-not $dpInfo) {
Warn " У формы нет основного реквизита — заимствовать нечего"
return
}
$dp = Collect-FormDataPaths $srcFormXmlPath $dpInfo.Name
$firstLevelNames = $dp.FirstLevel $firstLevelNames = $dp.FirstLevel
$deepPaths = $dp.DeepPaths $deepPaths = $dp.DeepPaths
Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths" Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths"
@@ -1461,19 +1823,15 @@ function Borrow-MainAttribute {
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true))) $objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
# 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)
$existingChildNames = @{} $existingChildNames = Get-OwnChildObjectNames $objFile
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
$existingChildNames[$nm.Groups[1].Value] = $true
}
}
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) }) $insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) }) $insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
# Generate full object XML with attributes and TS # Generate full object XML with attributes and TS
$contentSb = New-Object System.Text.StringBuilder $contentSb = New-Object System.Text.StringBuilder
foreach ($attr in $insertAttrs) { foreach ($attr in $insertAttrs) {
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $attrKind = if ($attr.Kind) { $attr.Kind } else { "Attribute" }
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $attrKind
$contentSb.AppendLine($attrXml) | Out-Null $contentSb.AppendLine($attrXml) | Out-Null
} }
foreach ($ts in $insertTS) { foreach ($ts in $insertTS) {
@@ -1498,17 +1856,9 @@ function Borrow-MainAttribute {
} }
} }
# Replace empty ChildObjects with adopted content # Добавить заимствованное содержимое в ChildObjects объекта (там уже может лежать <Form>)
if ($adoptedContent) { if ($adoptedContent) {
# Handle <ChildObjects/> (self-closing) $objContent = Insert-IntoOwnChildObjects $objContent "`r`n${adoptedContent}"
if ($objContent -match '<ChildObjects\s*/>') {
$objContent = $objContent -replace '<ChildObjects\s*/>', "<ChildObjects>`r`n${adoptedContent}`r`n`t`t</ChildObjects>"
}
# Handle <ChildObjects>...</ChildObjects> (may already have Form entry)
elseif ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
$existingInner = $Matches[1]
$objContent = $objContent -replace '(?s)<ChildObjects>(.*?)</ChildObjects>', "<ChildObjects>${existingInner}`r`n${adoptedContent}`r`n`t`t</ChildObjects>"
}
} }
$encBom = New-Object System.Text.UTF8Encoding($true) $encBom = New-Object System.Text.UTF8Encoding($true)
@@ -1521,6 +1871,21 @@ function Borrow-MainAttribute {
foreach ($ts in $srcTS) { foreach ($ts in $srcTS) {
foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml } foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml }
} }
# Типы из <Columns> основного реквизита формы: колонку мы переносим (Borrow-Form), значит и её
# тип должен быть заимствован — иначе колонка ссылается на DefinedType/справочник, которого в
# расширении нет. Конфигуратор поступает так же (эталон: DefinedTypes/Артикул при заимствовании
# формы заказа поставщику).
$srcFormForCols = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
if (Test-Path $srcFormForCols) {
$colsDoc = New-Object System.Xml.XmlDocument
$colsDoc.PreserveWhitespace = $true
$colsDoc.Load($srcFormForCols)
$colsInfo = Get-MainAttributeInfo $colsDoc.DocumentElement '\s+xmlns(?::\w+)?="[^"]*"'
if ($colsInfo) {
foreach ($m in [regex]::Matches($colsInfo.Xml, '(?s)<Columns>.*?</Columns>')) { $allTypeXmls += $m.Value }
}
}
$refTypes = Collect-ReferenceTypes $allTypeXmls $refTypes = Collect-ReferenceTypes $allTypeXmls
Info " Reference types to borrow: $($refTypes.Count)" Info " Reference types to borrow: $($refTypes.Count)"
@@ -1694,6 +2059,16 @@ function Build-BorrowedObjectXml {
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null $sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
} }
# Свойства, от которых зависят стандартные поля (см. $script:typeGateProps / $script:typesWithOwners)
foreach ($gp in @($script:typeGateProps[$typeName])) {
if ($gp -and $sourceProps.ContainsKey($gp)) {
$sb.AppendLine("`t`t`t<${gp}>$($sourceProps[$gp])</${gp}>") | Out-Null
}
}
if ($sourceProps.ContainsKey("__OwnersXml")) {
$sb.AppendLine("`t`t`t$($sourceProps['__OwnersXml'])") | Out-Null
}
$sb.AppendLine("`t`t</Properties>") | Out-Null $sb.AppendLine("`t`t</Properties>") | Out-Null
# ChildObjects (for types that need it) # ChildObjects (for types that need it)
@@ -1861,6 +2236,47 @@ foreach ($item in $items) {
} }
} }
# --- 14b. Владельцы заимствованных справочников ---
# Ссылка в <Owners> должна вести на объект, который в расширении есть: иначе платформа падает при
# загрузке (проверено — access violation, не сообщение об ошибке). Конфигуратор владельца
# заимствует (эталон Issue66Example7_1: вместе со справочником перенесён и его ПВХ-владелец).
# Проход общий и повторяется, пока находятся новые: у владельца может быть свой владелец.
$ownerPass = 0
while ($true) {
$ownerPass++
if ($ownerPass -gt 10) { break }
$newOwners = @()
foreach ($shell in (Get-ChildItem -Path $extDir -Filter "*.xml" -Recurse -File)) {
$shellText = [System.IO.File]::ReadAllText($shell.FullName)
if ($shellText -notmatch '<Owners>') { continue }
foreach ($om in [regex]::Matches($shellText, '<xr:Item[^>]*>(\w+)\.(\w+)</xr:Item>')) {
$oType = $om.Groups[1].Value; $oName = $om.Groups[2].Value
if (-not $childTypeDirMap.ContainsKey($oType)) { continue }
if (Test-ObjectBorrowed $oType $oName) { continue }
if ($newOwners | Where-Object { $_.T -eq $oType -and $_.N -eq $oName }) { continue }
$newOwners += @{ T = $oType; N = $oName }
}
}
if ($newOwners.Count -eq 0) { break }
foreach ($ow in $newOwners) {
$owSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$ow.T]) "$($ow.N).xml"
if (-not (Test-Path $owSrcFile)) {
Warn " Владелец $($ow.T).$($ow.N) не найден в источнике — ссылка останется висячей"
continue
}
$owSrc = Read-SourceObject $ow.T $ow.N
$owXml = Build-BorrowedObjectXml $ow.T $ow.N $owSrc.Uuid $owSrc.Properties
$owDir = Join-Path $extDir $childTypeDirMap[$ow.T]
if (-not (Test-Path $owDir)) { New-Item -ItemType Directory -Path $owDir -Force | Out-Null }
$owFile = Join-Path $owDir "$($ow.N).xml"
$owEnc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($owFile, $owXml, $owEnc)
Add-ToChildObjects $ow.T $ow.N
$script:borrowedFiles += $owFile
Info " Auto-borrowed owner: $($ow.T).$($ow.N)"
}
}
# --- 15. Save modified Configuration.xml --- # --- 15. Save modified Configuration.xml ---
$settings = New-Object System.Xml.XmlWriterSettings $settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true) $settings.Encoding = New-Object System.Text.UTF8Encoding($true)
@@ -1877,8 +2293,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"
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -23,7 +23,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
``` ```
## Mode A — обзор расширения ## Mode A — обзор расширения
@@ -50,8 +50,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -Exte
```powershell ```powershell
# Обзор — что изменено в расширении # Обзор — что изменено в расширении
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
# Проверка переноса — все ли #Вставка перенесены # Проверка переноса — все ли #Вставка перенесены
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode B
``` ```
+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
+8 -8
View File
@@ -33,39 +33,39 @@ allowed-tools:
| `Name` | Имя расширения (обязат.) | — | | `Name` | Имя расширения (обязат.) | — |
| `Synonym` | Синоним | = Name | | `Synonym` | Синоним | = Name |
| `NamePrefix` | Префикс собственных объектов | = Name + "_" | | `NamePrefix` | Префикс собственных объектов | = Name + "_" |
| `OutputDir` | Каталог для создания | `src` | | `OutputDir` | Каталог для создания; клади расширение в свой подкаталог, названный по имени: `src\cfe\<Name>` | `src` |
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` | | `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
| `Version` | Версия расширения | — | | `Version` | Версия расширения | — |
| `Vendor` | Поставщик | — | | `Vendor` | Поставщик | — |
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` | | `CompatibilityMode` | Режим совместимости; при заданном `ConfigPath` определяется по базовой конфигурации и этот параметр не нужен | `Version8_3_24` |
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — | | `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
| `NoRole` | Без основной роли | false | | `NoRole` | Без основной роли | false |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" -OutputDir "src\cfe\МоёРасширение" -ConfigPath "src\cf"
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Расширение для ERP с авто-определением совместимости из базовой конфигурации # Расширение для ERP с авто-определением совместимости из базовой конфигурации
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src ... -Name Расш1 -ConfigPath src\cf -OutputDir src\cfe\Расш1
# Расширение-исправление с явным режимом совместимости # Расширение-исправление с явным режимом совместимости
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src ... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src\cfe\Расш1
# Расширение-доработка с версией # Расширение-доработка с версией
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src ... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src\cfe\МоёРасширение
# Без роли, с явным префиксом # Без роли, с явным префиксом
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src ... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src\cfe\ИсправлениеБага
``` ```
## Верификация ## Верификация
``` ```
/cfe-validate <OutputDir> /cfe-validate <OutputDir> -ConfigPath <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}")
+9 -9
View File
@@ -110,36 +110,36 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Код перед записью # Код перед записью
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Перехват После на форме # Перехват После на форме
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
# Замена функции (ПродолжитьВызов) # Замена функции (ПродолжитьВызов)
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
# ИзменениеИКонтроль — копия тела для правки маркерами # ИзменениеИКонтроль — копия тела для правки маркерами
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
# ModulePath как путь к файлу модуля-источника (без -ConfigPath) # ModulePath как путь к файлу модуля-источника (без -ConfigPath)
... -ExtensionPath src\cfe\ИмяРасширения -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead ... -ExtensionPath src\cfe\extname -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
# Проверить все контролируемые методы расширения на дрейф # Проверить все контролируемые методы расширения на дрейф
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Check ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Check
# Актуализировать дрейфнувшие контролируемые методы пачкой # Актуализировать дрейфнувшие контролируемые методы пачкой
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Actualize ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Actualize
``` ```
## Верификация ## Верификация
``` ```
/cfe-validate <ExtensionPath> /cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
``` ```
@@ -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
+14 -3
View File
@@ -1,7 +1,7 @@
--- ---
name: cfe-validate name: cfe-validate
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30] argument-hint: <ExtensionPath> [-ConfigPath <ConfigDir>] [-Detailed] [-MaxErrors 30]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
@@ -17,13 +17,24 @@ allowed-tools:
| Параметр | Обяз. | Умолч. | Описание | | Параметр | Обяз. | Умолч. | Описание |
|---------------|:-----:|---------|-------------------------------------------------| |---------------|:-----:|---------|-------------------------------------------------|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения | | ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
| ConfigPath | нет | — | Каталог конфигурации, из которой заимствованы объекты |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок | | MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл | | OutFile | нет | — | Записать результат в файл |
### ConfigPath
Указывай всегда, когда конфигурация-источник доступна: без неё часть ошибок заимствованных форм не ловится, и расширение может пройти валидацию, а потом быть отвергнутым платформой при загрузке.
Если пользователь не указал путь — определи сам:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default`)
3. Возьми её поле `configSrc`
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname\Configuration.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname" -ConfigPath "src\cf"
``` ```
@@ -1,4 +1,4 @@
# cfe-validate v1.5 — Validate 1C configuration extension structure (CFE) # cfe-validate v1.10 — Validate 1C configuration extension structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -9,7 +9,11 @@ param(
[int]$MaxErrors = 30, [int]$MaxErrors = 30,
[string]$OutFile [string]$OutFile,
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
[string]$ConfigPath
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -89,6 +93,19 @@ $finalize = {
} }
} }
# --- Format version ---
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Reference tables --- # --- Reference tables ---
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' $guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' $identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
@@ -108,7 +125,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 +139,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"
@@ -144,6 +161,46 @@ $childTypeDirMap = @{
"IntegrationService"="IntegrationServices" "IntegrationService"="IntegrationServices"
} }
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
$generatedTypeCategories = @{
"Catalog" = @("Object","Ref","Selection","List","Manager")
"Document" = @("Object","Ref","Selection","List","Manager")
"Enum" = @("Ref","Manager","List")
"Constant" = @("Manager","ValueManager","ValueKey")
"Report" = @("Object","Manager")
"DataProcessor" = @("Object","Manager")
"ExchangePlan" = @("Object","Ref","Selection","List","Manager")
"Task" = @("Object","Ref","Selection","List","Manager")
"BusinessProcess" = @("Object","Ref","Selection","List","Manager","RoutePointRef")
"ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager","Characteristic")
"ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow")
"ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","DisplacingCalculationTypesRow","BaseCalculationTypes","BaseCalculationTypesRow","LeadingCalculationTypes","LeadingCalculationTypesRow")
"InformationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","RecordManager")
"AccumulationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey")
"AccountingRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","ExtDimensions")
"CalculationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","Recalcs")
"DocumentJournal" = @("Selection","List","Manager")
"Sequence" = @("Record","Manager","RecordSet")
"FilterCriterion" = @("Manager","List")
"SettingsStorage" = @("Manager")
"IntegrationService" = @("Manager")
"WSReference" = @("Manager")
"DefinedType" = @("DefinedType")
}
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
$script:standardObjectFields = @(
"Code","Description","Ref","Parent","Owner","DeletionMark","Predefined","IsFolder","LineNumber",
"Number","Date","Posted","PredefinedDataName","RegisterRecords","DataVersion","RowsCount",
"Код","Наименование","Ссылка","Родитель","Владелец","ПометкаУдаления","Предопределенный",
"ЭтоГруппа","НомерСтроки","Номер","Дата","Проведен","ИмяПредопределенныхДанных",
"Движения","ВерсияДанных","КоличествоСтрок"
)
# Valid enum values for extension properties # Valid enum values for extension properties
$validEnumValues = @{ $validEnumValues = @{
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1") "ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
@@ -195,11 +252,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
} }
$version = $root.GetAttribute("version") $version = $root.GetAttribute("version")
$versionRank = Get-FormatRank $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", "2.21")) { } elseif ($versionRank -eq 0) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). Report-Error "1. Malformed version '$version' (expected N.N)"
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)" } elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} }
# Must have Configuration child # Must have Configuration child
@@ -537,6 +598,7 @@ if ($script:stopped) { & $finalize; exit 1 }
# --- Check 9: Borrowed objects validation + Check 10: Sub-items --- # --- Check 9: Borrowed objects validation + Check 10: Sub-items ---
$script:enumValuesIndex = @{} $script:enumValuesIndex = @{}
$script:borrowedTSIndex = @{}
$script:formList = @() $script:formList = @()
# Helper: check if sub-item has explicit borrowed metadata # Helper: check if sub-item has explicit borrowed metadata
@@ -640,6 +702,25 @@ if ($childObjNode) {
} else { } else {
$borrowedOk++ $borrowedOk++
} }
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
$expectedCats = $generatedTypeCategories[$typeName]
if ($expectedCats) {
$objInfo = $objEl.SelectSingleNode("md:InternalInfo", $objNs)
$foundCats = @{}
if ($objInfo) {
foreach ($gt in $objInfo.SelectNodes("xr:GeneratedType", $objNs)) {
$cat = $gt.GetAttribute("category")
if ($cat) { $foundCats[$cat] = $true }
}
}
$missingCats = @($expectedCats | Where-Object { -not $foundCats.ContainsKey($_) })
if ($missingCats.Count -gt 0) {
Report-Error "9. Borrowed ${typeName}.${childName}: missing GeneratedType categor$(if ($missingCats.Count -eq 1) { 'y' } else { 'ies' }) $($missingCats -join ', ')"
$check9Ok = $false
}
}
} }
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
@@ -667,6 +748,12 @@ if ($childObjNode) {
$tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs) $tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs)
$tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) $tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs)
$tsLabel = if ($tsName) { $tsName.InnerText } else { "?" } $tsLabel = if ($tsName) { $tsName.InnerText } else { "?" }
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
if ($tsName) {
$tsKey = "${typeName}.${childName}"
if (-not $script:borrowedTSIndex.ContainsKey($tsKey)) { $script:borrowedTSIndex[$tsKey] = @{} }
$script:borrowedTSIndex[$tsKey][$tsName.InnerText] = $true
}
if (-not $tsInfo) { if (-not $tsInfo) {
Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo" Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo"
$check10Ok = $false $check10Ok = $false
@@ -896,6 +983,38 @@ foreach ($bf in $script:borrowedFormsWithTree) {
} }
} }
# Корень путей формы — имя её основного реквизита: «Объект» только у формы объекта, у формы
# списка «Список», у формы записи регистра «Запись». С зашитым «Объект» обе проверки ниже на
# таких формах молча не срабатывали. Ищем сначала в <Attributes> самой формы, потом в <BaseForm>.
$rootName = ""
$rootMatch = [regex]::Match($raw, '(?s)<Attribute name="([^"]+)"[^>]*>(?:(?!</Attribute>).)*?<MainAttribute>true</MainAttribute>')
if ($rootMatch.Success) { $rootName = $rootMatch.Groups[1].Value }
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
$acTables = @{}
if ($rootName) {
$rootPat = [regex]::Escape($rootName)
foreach ($m in [regex]::Matches($raw, "<AdditionalColumns table=`"${rootPat}\.(\w+)`"")) {
$acTables[$m.Groups[1].Value] = $true
}
}
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
# поэтому ошибка.
if ($acTables.Count -gt 0) {
$ownerKey = ($ctx -split '\.Form\.')[0]
$ownerTS = $script:borrowedTSIndex[$ownerKey]
foreach ($tblName in $acTables.Keys) {
$depCheckCount++
if (-not $ownerTS -or -not $ownerTS.ContainsKey($tblName)) {
Report-Error "12. ${ctx}: <AdditionalColumns table=`"${rootName}.${tblName}`"> — TabularSection.${tblName} not borrowed in extension"
$check12Ok = $false
}
}
}
foreach ($mi in $missingItems) { foreach ($mi in $missingItems) {
Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension" Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension"
$check12Ok = $false $check12Ok = $false
@@ -931,6 +1050,127 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
Report-OK "13. TypeLink: clean" Report-OK "13. TypeLink: clean"
} }
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
# не разрешится нигде, если Артикул — не реквизит объекта и не колонка из <Columns> самой формы.
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
if (-not $script:stopped -and $script:borrowedFormsWithTree.Count -gt 0) {
if (-not $ConfigPath) {
Out-Line "[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath"
} else {
$cfgRoot = $ConfigPath
if (-not [System.IO.Path]::IsPathRooted($cfgRoot)) { $cfgRoot = Join-Path (Get-Location).Path $cfgRoot }
if ((Test-Path $cfgRoot) -and -not (Test-Path $cfgRoot -PathType Container)) { $cfgRoot = Split-Path $cfgRoot -Parent }
if (-not (Test-Path (Join-Path $cfgRoot "Configuration.xml"))) {
Report-Warn "14. -ConfigPath '$ConfigPath': Configuration.xml не найден — проверка путей пропущена"
} else {
$check14Ok = $true
$pathCheckCount = 0
foreach ($bf in $script:borrowedFormsWithTree) {
$raw = $bf.RawText
$ctx = $bf.Context
# Корень путей — имя основного реквизита формы (см. проверку 12). Нет его ни в
# <Attributes> формы, ни в <BaseForm> — путей с корнем не бывает, проверять нечего.
$rootMatch14 = [regex]::Match($raw, '(?s)<Attribute name="([^"]+)"[^>]*>(?:(?!</Attribute>).)*?<MainAttribute>true</MainAttribute>')
if (-not $rootMatch14.Success) { continue }
$rootName = $rootMatch14.Groups[1].Value
# У динамического списка набор полей — результат его запроса, а не состав объекта:
# туда входят и стандартные поля списка (Ref, Date, DefaultPicture), и псевдонимы
# запроса. Сверять такие пути с ChildObjects объекта нельзя — будут ложные ошибки
# (корпусная проверка: 3383 таких сегмента на 1094 формах списка УТ).
if ($rootMatch14.Value -match '>cfg:DynamicList<') { continue }
$ownerKey = ($ctx -split '\.Form\.')[0]
$ownerParts = $ownerKey -split '\.', 2
if ($ownerParts.Count -lt 2) { continue }
$ownerType = $ownerParts[0]; $ownerName = $ownerParts[1]
$ownerDir = $childTypeDirMap[$ownerType]
if (-not $ownerDir) { continue }
$srcObjFile = Join-Path (Join-Path $cfgRoot $ownerDir) "${ownerName}.xml"
if (-not (Test-Path $srcObjFile)) {
Report-Warn "14. ${ctx}: объект-источник не найден в конфигурации ($ownerDir/${ownerName}.xml)"
continue
}
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
$srcNames = @{}
$srcTSColumns = @{}
$srcDoc = New-Object System.Xml.XmlDocument
$srcDoc.PreserveWhitespace = $false
$srcDoc.Load($srcObjFile)
$srcObjEl = $null
foreach ($c in $srcDoc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $srcObjEl = $c; break }
}
$srcChildObjects = if ($srcObjEl) { $srcObjEl.SelectSingleNode("*[local-name()='ChildObjects']") } else { $null }
if ($srcChildObjects) {
foreach ($sub in $srcChildObjects.ChildNodes) {
if ($sub.NodeType -ne 'Element') { continue }
# У регистра дочерние объекты — Dimension/Resource, а не Attribute: без них замена
# корня превратила бы тихий пропуск в ложные ошибки на форме записи.
if ($sub.LocalName -notin @('Attribute','Dimension','Resource','TabularSection')) { continue }
$nameNode = $sub.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
if (-not $nameNode) { continue }
$subName = $nameNode.InnerText.Trim()
$srcNames[$subName] = $true
if ($sub.LocalName -ne 'TabularSection') { continue }
$cols = @{}
foreach ($colName in $sub.SelectNodes("*[local-name()='ChildObjects']/*[local-name()='Attribute']/*[local-name()='Properties']/*[local-name()='Name']")) {
$cols[$colName.InnerText.Trim()] = $true
}
$srcTSColumns[$subName] = $cols
}
}
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
$rootPat14 = [regex]::Escape($rootName)
foreach ($acm in [regex]::Matches($raw, "(?s)<AdditionalColumns table=`"${rootPat14}\.(\w+)`">(.*?)</AdditionalColumns>")) {
$tbl = $acm.Groups[1].Value
if (-not $srcTSColumns.ContainsKey($tbl)) { $srcTSColumns[$tbl] = @{} }
foreach ($cm in [regex]::Matches($acm.Groups[2].Value, '<Column name="(\w+)"')) {
$srcTSColumns[$tbl][$cm.Groups[1].Value] = $true
}
}
$badPaths = @{}
foreach ($m in [regex]::Matches($raw, "<(?:\w+:)?\w*DataPath[^>]*>${rootPat14}\.([^<]+)</(?:\w+:)?\w*DataPath>")) {
$segments = $m.Groups[1].Value -split '\.'
$seg0 = $segments[0]
$pathCheckCount++
if ($script:standardObjectFields -contains $seg0) { continue }
if (-not $srcNames.ContainsKey($seg0)) {
$badPaths["${rootName}.${seg0}"] = "у ${ownerKey} нет такого реквизита или табличной части"
continue
}
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
# он ведёт в чужой объект, и это уже другая проверка.
if ($segments.Count -lt 2 -or -not $srcTSColumns.ContainsKey($seg0)) { continue }
$seg1 = $segments[1]
if ($script:standardObjectFields -contains $seg1) { continue }
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
if ($seg1 -like "Total*" -and $srcTSColumns[$seg0].ContainsKey($seg1.Substring(5))) { continue }
if (-not $srcTSColumns[$seg0].ContainsKey($seg1)) {
$badPaths["${rootName}.${seg0}.${seg1}"] = "у табличной части ${seg0} нет колонки ${seg1}, и <Columns> формы её не объявляет"
}
}
foreach ($bad in ($badPaths.Keys | Sort-Object)) {
Report-Error "14. ${ctx}: путь '${bad}' — $($badPaths[$bad])"
$check14Ok = $false
}
}
if ($check14Ok) {
Report-OK "14. Object paths vs source config: $pathCheckCount checked"
}
}
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here --- # --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
$extRootDir = Split-Path $resolvedPath -Parent $extRootDir = Split-Path $resolvedPath -Parent
$ctrlCount = 0 $ctrlCount = 0
@@ -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.10 — Validate 1C configuration extension XML structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
# 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',
@@ -73,6 +96,50 @@ CHILD_TYPE_DIR_MAP = {
'IntegrationService': 'IntegrationServices', 'IntegrationService': 'IntegrationServices',
} }
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
GENERATED_TYPE_CATEGORIES = {
'Catalog': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
'Document': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
'Enum': ['Ref', 'Manager', 'List'],
'Constant': ['Manager', 'ValueManager', 'ValueKey'],
'Report': ['Object', 'Manager'],
'DataProcessor': ['Object', 'Manager'],
'ExchangePlan': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
'Task': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
'BusinessProcess': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'RoutePointRef'],
'ChartOfCharacteristicTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'Characteristic'],
'ChartOfAccounts': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'ExtDimensionTypes', 'ExtDimensionTypesRow'],
'ChartOfCalculationTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'DisplacingCalculationTypes', 'DisplacingCalculationTypesRow', 'BaseCalculationTypes', 'BaseCalculationTypesRow', 'LeadingCalculationTypes', 'LeadingCalculationTypesRow'],
'InformationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'RecordManager'],
'AccumulationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey'],
'AccountingRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'ExtDimensions'],
'CalculationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'Recalcs'],
'DocumentJournal': ['Selection', 'List', 'Manager'],
'Sequence': ['Record', 'Manager', 'RecordSet'],
'FilterCriterion': ['Manager', 'List'],
'SettingsStorage': ['Manager'],
'IntegrationService': ['Manager'],
'WSReference': ['Manager'],
'DefinedType': ['DefinedType'],
}
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
# Основной реквизит формы: <Attribute name="X"> с <MainAttribute>true</MainAttribute> внутри
MAIN_ATTR_RE = re.compile(
r'<Attribute name=\"([^\"]+)\"[^>]*>(?:(?!</Attribute>).)*?<MainAttribute>true</MainAttribute>', re.DOTALL)
STANDARD_OBJECT_FIELDS = {
'Code', 'Description', 'Ref', 'Parent', 'Owner', 'DeletionMark', 'Predefined', 'IsFolder', 'LineNumber',
'Number', 'Date', 'Posted', 'PredefinedDataName', 'RegisterRecords', 'DataVersion', 'RowsCount',
'Код', 'Наименование', 'Ссылка', 'Родитель', 'Владелец', 'ПометкаУдаления', 'Предопределенный',
'ЭтоГруппа', 'НомерСтроки', 'Номер', 'Дата', 'Проведен', 'ИмяПредопределенныхДанных',
'Движения', 'ВерсияДанных', 'КоличествоСтрок',
}
# Valid enum values for extension properties # Valid enum values for extension properties
VALID_ENUM_VALUES = { VALID_ENUM_VALUES = {
'ConfigurationExtensionCompatibilityMode': [ 'ConfigurationExtensionCompatibilityMode': [
@@ -94,6 +161,20 @@ VALID_ENUM_VALUES = {
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses' EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
# ── Format version ───────────────────────────────────────────
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
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
class Reporter: class Reporter:
def __init__(self, max_errors, detailed=False): def __init__(self, max_errors, detailed=False):
@@ -154,11 +235,15 @@ 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() # Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
parser.add_argument('-ConfigPath', dest='ConfigPath', default='')
args = ci_parse_args(parser)
extension_path = args.ExtensionPath extension_path = args.ExtensionPath
max_errors = args.MaxErrors max_errors = args.MaxErrors
out_file = args.OutFile out_file = args.OutFile
config_path_arg = args.ConfigPath
# --- Resolve path --- # --- Resolve path ---
if not os.path.isabs(extension_path): if not os.path.isabs(extension_path):
@@ -214,11 +299,17 @@ def main():
check1_ok = False check1_ok = False
version = root.get('version', '') version = root.get('version', '')
version_rank = format_rank(version)
if not version: if not version:
r.warn('1. Missing version attribute on MetaDataObject') r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'): elif version_rank == 0:
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). r.error(f"1. Malformed version '{version}' (expected N.N)")
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)") elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
r.warn(f"1. Format version '{version}' is below the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
r.warn(f"1. Format version '{version}' is above the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
# Must have Configuration child # Must have Configuration child
cfg_node = None cfg_node = None
@@ -537,6 +628,7 @@ def main():
MD = NS['md'] MD = NS['md']
XR = NS['xr'] XR = NS['xr']
enum_values_index = {} enum_values_index = {}
borrowed_ts_index = {}
form_list = [] form_list = []
def is_borrowed_sub_item(sub_item): def is_borrowed_sub_item(sub_item):
@@ -636,6 +728,23 @@ def main():
else: else:
borrowed_ok_count += 1 borrowed_ok_count += 1
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
expected_cats = GENERATED_TYPE_CATEGORIES.get(type_name)
if expected_cats:
obj_info = obj_el.find(f'{{{MD}}}InternalInfo')
found_cats = set()
if obj_info is not None:
for gt in obj_info.findall(f'{{{XR}}}GeneratedType'):
cat = gt.get('category')
if cat:
found_cats.add(cat)
missing_cats = [c for c in expected_cats if c not in found_cats]
if missing_cats:
word = 'category' if len(missing_cats) == 1 else 'categories'
r.error(f"9. Borrowed {type_name}.{child_name}: missing GeneratedType {word} {', '.join(missing_cats)}")
check9_ok = False
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects') obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects')
if obj_child_objects is not None: if obj_child_objects is not None:
@@ -663,6 +772,9 @@ def main():
ts_info = sub_item.find(f'{{{MD}}}InternalInfo') ts_info = sub_item.find(f'{{{MD}}}InternalInfo')
ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name')
ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?' ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?'
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
if ts_name_el is not None and ts_name_el.text:
borrowed_ts_index.setdefault(f'{type_name}.{child_name}', {})[ts_name_el.text.strip()] = True
if ts_info is None: if ts_info is None:
r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo') r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo')
check10_ok = False check10_ok = False
@@ -855,6 +967,29 @@ def main():
elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}): elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}):
missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}") missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}")
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
# поэтому ошибка.
# Корень путей формы — имя её основного реквизита: «Объект» только у формы объекта, у формы
# списка «Список», у формы записи регистра «Запись». С зашитым «Объект» обе проверки на
# таких формах молча не срабатывали. Ищем сначала в <Attributes> формы, потом в <BaseForm>.
root_match = MAIN_ATTR_RE.search(raw)
root_name = root_match.group(1) if root_match else ""
ac_tables = set()
if root_name:
ac_tables = set(re.findall(r'<AdditionalColumns table="' + re.escape(root_name) + r'\.(\w+)"', raw))
if ac_tables:
owner_key = ctx.split('.Form.')[0]
owner_ts = borrowed_ts_index.get(owner_key, {})
for tbl_name in sorted(ac_tables):
dep_check_count += 1
if tbl_name not in owner_ts:
r.error(f'12. {ctx}: <AdditionalColumns table="{root_name}.{tbl_name}"> — TabularSection.{tbl_name} not borrowed in extension')
check12_ok = False
for mi in missing_items: for mi in missing_items:
r.warn(f'12. {ctx}: references {mi} not borrowed in extension') r.warn(f'12. {ctx}: references {mi} not borrowed in extension')
check12_ok = False check12_ok = False
@@ -886,6 +1021,130 @@ def main():
elif check13_ok: elif check13_ok:
r.ok('13. TypeLink: clean') r.ok('13. TypeLink: clean')
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
# не разрешится нигде, если Артикул — не колонка ТЧ и не колонка из <Columns> самой формы.
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
if not r.stopped and borrowed_forms_with_tree:
if not config_path_arg:
r.out('[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath')
else:
cfg_root = config_path_arg
if not os.path.isabs(cfg_root):
cfg_root = os.path.join(os.getcwd(), cfg_root)
if os.path.exists(cfg_root) and not os.path.isdir(cfg_root):
cfg_root = os.path.dirname(cfg_root)
if not os.path.isfile(os.path.join(cfg_root, 'Configuration.xml')):
r.warn(f"14. -ConfigPath '{config_path_arg}': Configuration.xml не найден — проверка путей пропущена")
else:
check14_ok = True
path_check_count = 0
for bf in borrowed_forms_with_tree:
raw = bf['RawText']
ctx = bf['Context']
# Корень путей — имя основного реквизита формы (см. проверку 12). Нет его ни в
# <Attributes> формы, ни в <BaseForm> — путей с корнем не бывает, проверять нечего.
root_match14 = MAIN_ATTR_RE.search(raw)
if root_match14 is None:
continue
root_name14 = root_match14.group(1)
# У динамического списка набор полей — результат его запроса, а не состав объекта:
# туда входят и стандартные поля списка (Ref, Date, DefaultPicture), и псевдонимы
# запроса. Сверять такие пути с ChildObjects объекта нельзя — будут ложные ошибки
# (корпусная проверка: 3383 таких сегмента на 1094 формах списка УТ).
if '>cfg:DynamicList<' in root_match14.group(0):
continue
owner_key = ctx.split('.Form.')[0]
owner_parts = owner_key.split('.', 1)
if len(owner_parts) < 2:
continue
owner_type, owner_name = owner_parts
owner_dir = CHILD_TYPE_DIR_MAP.get(owner_type)
if not owner_dir:
continue
src_obj_file = os.path.join(cfg_root, owner_dir, f'{owner_name}.xml')
if not os.path.isfile(src_obj_file):
r.warn(f'14. {ctx}: объект-источник не найден в конфигурации ({owner_dir}/{owner_name}.xml)')
continue
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
src_names = set()
src_ts_columns = {}
src_tree = etree.parse(src_obj_file, etree.XMLParser(remove_blank_text=True))
src_obj_el = None
for c in src_tree.getroot():
if isinstance(c.tag, str):
src_obj_el = c
break
src_child_objects = src_obj_el.find(f'{{{MD}}}ChildObjects') if src_obj_el is not None else None
if src_child_objects is not None:
for sub in src_child_objects:
if not isinstance(sub.tag, str):
continue
sub_ln = etree.QName(sub.tag).localname
# У регистра дочерние объекты — Dimension/Resource, а не Attribute: без них
# замена корня превратила бы тихий пропуск в ложные ошибки на форме записи.
if sub_ln not in ('Attribute', 'Dimension', 'Resource', 'TabularSection'):
continue
name_el = sub.find(f'{{{MD}}}Properties/{{{MD}}}Name')
if name_el is None or not name_el.text:
continue
sub_name = name_el.text.strip()
src_names.add(sub_name)
if sub_ln != 'TabularSection':
continue
cols = set()
for col_name in sub.findall(f'{{{MD}}}ChildObjects/{{{MD}}}Attribute/{{{MD}}}Properties/{{{MD}}}Name'):
if col_name.text:
cols.add(col_name.text.strip())
src_ts_columns[sub_name] = cols
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
root_pat14 = re.escape(root_name14)
for acm in re.finditer(r'<AdditionalColumns table="' + root_pat14 + r'\.(\w+)">(.*?)</AdditionalColumns>', raw, re.DOTALL):
tbl = acm.group(1)
cols = src_ts_columns.setdefault(tbl, set())
for cm in re.finditer(r'<Column name="(\w+)"', acm.group(2)):
cols.add(cm.group(1))
bad_paths = {}
for m in re.finditer(r'<(?:\w+:)?\w*DataPath[^>]*>' + root_pat14 + r'\.([^<]+)</(?:\w+:)?\w*DataPath>', raw):
segments = m.group(1).split('.')
seg0 = segments[0]
path_check_count += 1
if seg0 in STANDARD_OBJECT_FIELDS:
continue
if seg0 not in src_names:
bad_paths[f'{root_name14}.{seg0}'] = f'у {owner_key} нет такого реквизита или табличной части'
continue
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
# он ведёт в чужой объект, и это уже другая проверка.
if len(segments) < 2 or seg0 not in src_ts_columns:
continue
seg1 = segments[1]
if seg1 in STANDARD_OBJECT_FIELDS:
continue
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
if seg1.startswith('Total') and seg1[5:] in src_ts_columns[seg0]:
continue
if seg1 not in src_ts_columns[seg0]:
bad_paths[f'{root_name14}.{seg0}.{seg1}'] = f'у табличной части {seg0} нет колонки {seg1}, и <Columns> формы её не объявляет'
for bad in sorted(bad_paths):
r.error(f"14. {ctx}: путь '{bad}'{bad_paths[bad]}")
check14_ok = False
if check14_ok:
r.ok(f'14. Object paths vs source config: {path_check_count} checked')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here --- # --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
ctrl_count = 0 ctrl_count = 0
for dp, _dn, files in os.walk(config_dir): for dp, _dn, files in os.walk(config_dir):
@@ -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.20 — 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 не исполняется.
<# <#
@@ -110,6 +110,12 @@ param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$UpdateDB, [switch]$UpdateDB,
[Parameter(Mandatory=$false)]
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но в логе есть отбраковка.
[switch]$StrictLog,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(), [string[]]$AdditionalV8Arguments = @(),
@@ -394,6 +400,41 @@ function Write-PlatformOutput {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
# Строки лога, о которых платформа сообщает, НЕ поднимая код возврата: метаданные отброшены или
# конфигурация нерабочая, а операция при этом «успешна». Возвращает подошедшие строки.
#
# Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки автономны).
# Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Find-SilentRejections {
param([string]$LogText)
$patterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу',
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в рантайме.
# Обрезано до инвариантной части — конкретная версия в сообщении меняется.
'Для работы с конфигурацией необходима версия платформы не меньше'
)
$found = @()
if ($LogText) {
foreach ($line in ($LogText -split "`r?`n")) {
foreach ($pat in $patterns) {
if ($line -match [regex]::Escape($pat)) {
$found += $line.Trim()
break
}
}
}
}
# Возвращаем массив БЕЗ запятой-обёртки: вызывающий берёт результат в @(), а `return ,$found`
# дал бы массив из одного пустого массива — фантомное срабатывание на чистом логе.
return $found
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") { if ($engine -eq "ibcmd") {
@@ -667,6 +708,7 @@ try {
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
} }
$logContent = $null
if (Test-Path $outFile) { if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) { if ($logContent) {
@@ -677,6 +719,16 @@ try {
} }
Write-PlatformOutput $__v8.Output Write-PlatformOutput $__v8.Output
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
$silentFailures = @(Find-SilentRejections $logContent)
if ($silentFailures.Count -gt 0) {
Write-Host "[warning] platform reported success, but the log contains $($silentFailures.Count) problem(s):" -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
exit $exitCode exit $exitCode
} finally { } finally {
@@ -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.20 — 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."""
@@ -322,6 +344,38 @@ def print_platform_output(result):
print("--- End ---") print("--- End ---")
def find_silent_rejections(log_text):
"""Строки лога, о которых платформа сообщает, НЕ поднимая код возврата.
Метаданные отброшены или конфигурация нерабочая, а операция при этом «успешна».
Возвращает подошедшие строки.
Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки
автономны). Держать копии одинаковыми сознательно: разошедшиеся копии сводят на нет
весь смысл.
"""
patterns = [
"Неверное свойство объекта метаданных",
"не входит в состав объекта метаданных",
"Неизвестное имя типа",
"Неизвестный объект метаданных",
"Ни один из документов не является регистратором для регистра",
"Неверное значение перечисления",
"не может быть приведен к типу",
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в
# рантайме. Обрезано до инвариантной части — конкретная версия в сообщении меняется.
"Для работы с конфигурацией необходима версия платформы не меньше",
]
found = []
if log_text:
for line in log_text.splitlines():
for pat in patterns:
if pat in line:
found.append(line.strip())
break
return found
def run_ibcmd(cmd, has_username=False, warn_no_user=True): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -424,13 +478,17 @@ def main():
) )
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)") parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load") parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но в логе есть отбраковка.
parser.add_argument("-StrictLog", action="store_true")
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
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")
@@ -683,6 +741,7 @@ def main():
else: else:
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
log_content = ""
if os.path.isfile(out_file): if os.path.isfile(out_file):
try: try:
with open(out_file, "r", encoding="utf-8-sig") as f: with open(out_file, "r", encoding="utf-8-sig") as f:
@@ -695,6 +754,21 @@ def main():
pass pass
print_platform_output(result) print_platform_output(result)
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
silent_failures = find_silent_rejections(log_content)
if silent_failures:
print(
f"[warning] platform reported success, but the log contains "
f"{len(silent_failures)} problem(s):"
)
for line in silent_failures:
print(f" {line}")
if args.StrictLog and exit_code == 0:
exit_code = 1
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
@@ -1,4 +1,4 @@
# db-load-xml v1.19 — Load 1C configuration from XML files # db-load-xml v1.21 — 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 не исполняется.
<# <#
@@ -416,6 +416,41 @@ function Write-PlatformOutput {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
# Строки лога, о которых платформа сообщает, НЕ поднимая код возврата: метаданные отброшены или
# конфигурация нерабочая, а операция при этом «успешна». Возвращает подошедшие строки.
#
# Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки автономны).
# Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Find-SilentRejections {
param([string]$LogText)
$patterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу',
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в рантайме.
# Обрезано до инвариантной части — конкретная версия в сообщении меняется.
'Для работы с конфигурацией необходима версия платформы не меньше'
)
$found = @()
if ($LogText) {
foreach ($line in ($LogText -split "`r?`n")) {
foreach ($pat in $patterns) {
if ($line -match [regex]::Escape($pat)) {
$found += $line.Trim()
break
}
}
}
}
# Возвращаем массив БЕЗ запятой-обёртки: вызывающий берёт результат в @(), а `return ,$found`
# дал бы массив из одного пустого массива — фантомное срабатывание на чистом логе.
return $found
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
@@ -607,28 +642,7 @@ try {
} }
# --- Scan log for silent rejections --- # --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0. $silentFailures = @(Find-SilentRejections $logContent)
# These patterns flag cases where metadata was dropped or rejected silently.
$fatalLogPatterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу'
)
$silentFailures = @()
if ($logContent) {
foreach ($line in ($logContent -split "`r?`n")) {
foreach ($pat in $fatalLogPatterns) {
if ($line -match [regex]::Escape($pat)) {
$silentFailures += $line.Trim()
break
}
}
}
}
# --- Result --- # --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any # Default: mirror platform's verdict via exit code. Log content (including any
@@ -647,10 +661,11 @@ try {
} }
Write-PlatformOutput $__v8.Output Write-PlatformOutput $__v8.Output
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
if ($silentFailures.Count -gt 0) { if ($silentFailures.Count -gt 0) {
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs" Write-Host "[warning] platform reported success, but the log contains $($silentFailures.Count) problem(s):" -ForegroundColor Yellow
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
Write-Host $msg -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow } foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 } if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
} }
@@ -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.21 — 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."""
@@ -322,6 +344,38 @@ def print_platform_output(result):
print("--- End ---") print("--- End ---")
def find_silent_rejections(log_text):
"""Строки лога, о которых платформа сообщает, НЕ поднимая код возврата.
Метаданные отброшены или конфигурация нерабочая, а операция при этом «успешна».
Возвращает подошедшие строки.
Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки
автономны). Держать копии одинаковыми сознательно: разошедшиеся копии сводят на нет
весь смысл.
"""
patterns = [
"Неверное свойство объекта метаданных",
"не входит в состав объекта метаданных",
"Неизвестное имя типа",
"Неизвестный объект метаданных",
"Ни один из документов не является регистратором для регистра",
"Неверное значение перечисления",
"не может быть приведен к типу",
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в
# рантайме. Обрезано до инвариантной части — конкретная версия в сообщении меняется.
"Для работы с конфигурацией необходима версия платформы не меньше",
]
found = []
if log_text:
for line in log_text.splitlines():
for pat in patterns:
if pat in line:
found.append(line.strip())
break
return found
def run_ibcmd(cmd, has_username=False, warn_no_user=True): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -413,7 +467,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")
@@ -614,22 +668,7 @@ def main():
# --- Scan log for silent rejections --- # --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0. # Platform often writes load-time rejections into /Out but exits with code 0.
# These patterns flag cases where metadata was dropped or rejected silently. # These patterns flag cases where metadata was dropped or rejected silently.
fatal_log_patterns = [ silent_failures = find_silent_rejections(log_content)
"Неверное свойство объекта метаданных",
"не входит в состав объекта метаданных",
"Неизвестное имя типа",
"Неизвестный объект метаданных",
"Ни один из документов не является регистратором для регистра",
"Неверное значение перечисления",
"не может быть приведен к типу",
]
silent_failures = []
if log_content:
for line in log_content.splitlines():
for pat in fatal_log_patterns:
if pat in line:
silent_failures.append(line.strip())
break
# --- Result --- # --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any # Default: mirror platform's verdict via exit code. Log content (including any
@@ -646,15 +685,19 @@ def main():
print("--- End ---") print("--- End ---")
print_platform_output(result) print_platform_output(result)
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
# Поток — stdout, как у PS1-порта: предупреждение относится к содержимому загрузки, а не к
# отказу навыка, и при code 0 остаётся предупреждением. Раньше py писал его в stderr —
# наблюдаемое поведение портов расходилось, и один кейс не мог проверить оба.
if silent_failures: if silent_failures:
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
print( print(
f"[warning] log contains {len(silent_failures)} rejection(s) — " f"[warning] platform reported success, but the log contains "
f"platform loaded config but dropped properties/refs{suffix}", f"{len(silent_failures)} problem(s):"
file=sys.stderr,
) )
for f in silent_failures: for f in silent_failures:
print(f" {f}", file=sys.stderr) print(f" {f}")
if args.StrictLog and exit_code == 0: if args.StrictLog and exit_code == 0:
exit_code = 1 exit_code = 1
+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")
+53 -1
View File
@@ -1,4 +1,4 @@
# db-update v1.13 — Update 1C database configuration # db-update v1.15 — 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 не исполняется.
<# <#
@@ -91,6 +91,12 @@ param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$WarningsAsErrors, [switch]$WarningsAsErrors,
[Parameter(Mandatory=$false)]
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но в логе есть отбраковка.
[switch]$StrictLog,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(), [string[]]$AdditionalV8Arguments = @(),
@@ -395,6 +401,41 @@ function Write-PlatformOutput {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
# Строки лога, о которых платформа сообщает, НЕ поднимая код возврата: метаданные отброшены или
# конфигурация нерабочая, а операция при этом «успешна». Возвращает подошедшие строки.
#
# Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки автономны).
# Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Find-SilentRejections {
param([string]$LogText)
$patterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу',
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в рантайме.
# Обрезано до инвариантной части — конкретная версия в сообщении меняется.
'Для работы с конфигурацией необходима версия платформы не меньше'
)
$found = @()
if ($LogText) {
foreach ($line in ($LogText -split "`r?`n")) {
foreach ($pat in $patterns) {
if ($line -match [regex]::Escape($pat)) {
$found += $line.Trim()
break
}
}
}
}
# Возвращаем массив БЕЗ запятой-обёртки: вызывающий берёт результат в @(), а `return ,$found`
# дал бы массив из одного пустого массива — фантомное срабатывание на чистом логе.
return $found
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
@@ -496,6 +537,7 @@ try {
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
} }
$logContent = $null
if (Test-Path $outFile) { if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) { if ($logContent) {
@@ -506,6 +548,16 @@ try {
} }
Write-PlatformOutput $__v8.Output Write-PlatformOutput $__v8.Output
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
$silentFailures = @(Find-SilentRejections $logContent)
if ($silentFailures.Count -gt 0) {
Write-Host "[warning] platform reported success, but the log contains $($silentFailures.Count) problem(s):" -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
exit $exitCode exit $exitCode
} finally { } finally {
+76 -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.15 — 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."""
@@ -322,6 +344,38 @@ def print_platform_output(result):
print("--- End ---") print("--- End ---")
def find_silent_rejections(log_text):
"""Строки лога, о которых платформа сообщает, НЕ поднимая код возврата.
Метаданные отброшены или конфигурация нерабочая, а операция при этом «успешна».
Возвращает подошедшие строки.
Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки
автономны). Держать копии одинаковыми сознательно: разошедшиеся копии сводят на нет
весь смысл.
"""
patterns = [
"Неверное свойство объекта метаданных",
"не входит в состав объекта метаданных",
"Неизвестное имя типа",
"Неизвестный объект метаданных",
"Ни один из документов не является регистратором для регистра",
"Неверное значение перечисления",
"не может быть приведен к типу",
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в
# рантайме. Обрезано до инвариантной части — конкретная версия в сообщении меняется.
"Для работы с конфигурацией необходима версия платформы не меньше",
]
found = []
if log_text:
for line in log_text.splitlines():
for pat in patterns:
if pat in line:
found.append(line.strip())
break
return found
def run_ibcmd(cmd, has_username=False, warn_no_user=True): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -389,13 +443,17 @@ def main():
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"]) parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
parser.add_argument("-Server", action="store_true") parser.add_argument("-Server", action="store_true")
parser.add_argument("-WarningsAsErrors", action="store_true") parser.add_argument("-WarningsAsErrors", action="store_true")
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но в логе есть отбраковка.
parser.add_argument("-StrictLog", action="store_true")
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
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")
@@ -505,6 +563,7 @@ def main():
else: else:
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
log_content = ""
if os.path.isfile(out_file): if os.path.isfile(out_file):
try: try:
with open(out_file, "r", encoding="utf-8-sig") as f: with open(out_file, "r", encoding="utf-8-sig") as f:
@@ -517,6 +576,21 @@ def main():
pass pass
print_platform_output(result) print_platform_output(result)
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
silent_failures = find_silent_rejections(log_content)
if silent_failures:
print(
f"[warning] platform reported success, but the log contains "
f"{len(silent_failures)} problem(s):"
)
for line in silent_failures:
print(f" {line}")
if args.StrictLog and exit_code == 0:
exit_code = 1
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
@@ -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,4 +1,4 @@
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build # stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -163,14 +163,35 @@ function Format-ArgsForDisplay {
} }
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("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. Scan XML files for reference types --- # --- 1. Scan XML files for reference types ---
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...) $typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
# Версия формата заглушечной конфигурации. Платформа грузит формат не новее себя, поэтому зашитая
# версия ломала бы сборку исходников более старого формата на соответствующей ей платформе. Берём
# версию из корня собираемого объекта (ExternalDataProcessor/ExternalReport); вложенные файлы —
# запасной вариант, если корень почему-то не попался.
$srcRootVersion = ""
$srcAnyVersion = ""
$xmlFiles = Get-ChildItem -Path $SourceDir -Filter "*.xml" -Recurse -File $xmlFiles = Get-ChildItem -Path $SourceDir -Filter "*.xml" -Recurse -File
foreach ($f in $xmlFiles) { foreach ($f in $xmlFiles) {
$content = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8) $content = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8)
if ($content -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') {
$ver = $Matches[1]
if (-not $srcAnyVersion) { $srcAnyVersion = $ver }
if (-not $srcRootVersion -and $content -match '<(ExternalDataProcessor|ExternalReport)[ >]') {
$srcRootVersion = $ver
}
}
# Ref types: cfg:CatalogRef.XXX or d5p1:CatalogRef.XXX (and similar depth prefixes d4p1, d3p1, etc.) # Ref types: cfg:CatalogRef.XXX or d5p1:CatalogRef.XXX (and similar depth prefixes d4p1, d3p1, etc.)
$refPattern = '(?:cfg:|d\dp1:)(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.([A-Za-z\u0400-\u04FF\d_]+)' $refPattern = '(?:cfg:|d\dp1:)(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.([A-Za-z\u0400-\u04FF\d_]+)'
foreach ($m in [regex]::Matches($content, $refPattern)) { foreach ($m in [regex]::Matches($content, $refPattern)) {
@@ -337,7 +358,24 @@ if ($hasRefTypes) {
$cfgDir = Join-Path $TempBasePath "cfg" $cfgDir = Join-Path $TempBasePath "cfg"
New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null
$ns = '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"' # Заглушке нужна САМАЯ НИЗКАЯ работающая версия, а не версия исходников: ограничение платформы
# одностороннее — она читает формат не новее себя. Отсюда min(версия исходников, 2.17): на 2.17+
# заглушка остаётся 2.17 (как было), а под исходники 2.13-2.16 опускается до их версии, иначе
# конфигурация не загрузится платформой, которая эти исходники и выгрузила («Неизвестная версия
# формата 2.17 загружаемого файла», замерено на 8.3.20).
#
$srcVersion = if ($srcRootVersion) { $srcRootVersion } elseif ($srcAnyVersion) { $srcAnyVersion } else { "2.17" }
$srcRank = Get-FormatRank $srcVersion
$stubFormatVersion = if ($srcRank -gt 0 -and $srcRank -lt (Get-FormatRank "2.17")) { $srcVersion } else { "2.17" }
# Режим совместимости заглушки — по той же логике. Платформа отказывается работать с
# конфигурацией, чей режим выше её самой («Для работы с конфигурацией необходима версия
# платформы не меньше, чем 8.3.24»), и тогда объекты заглушки в базу не попадают: загрузка
# рапортует успех, а сборка падает на «Неизвестное имя типа». Ступени — лестница версий
# формата из docs/1c-configuration-spec.md.
$compatByFormat = @{ "2.13" = "Version8_3_20"; "2.14" = "Version8_3_21"; "2.15" = "Version8_3_22"; "2.16" = "Version8_3_23" }
$stubCompatMode = if ($compatByFormat.ContainsKey($stubFormatVersion)) { $compatByFormat[$stubFormatVersion] } else { "Version8_3_24" }
$ns = '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="' + $stubFormatVersion + '"'
# GeneratedType definitions per metadata type # GeneratedType definitions per metadata type
$gtDefs = @{ $gtDefs = @{
@@ -521,7 +559,7 @@ if ($hasRefTypes) {
<Synonym/> <Synonym/>
<Comment/> <Comment/>
<NamePrefix/> <NamePrefix/>
<ConfigurationExtensionCompatibilityMode>Version8_3_24</ConfigurationExtensionCompatibilityMode> <ConfigurationExtensionCompatibilityMode>$stubCompatMode</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>
@@ -572,7 +610,7 @@ if ($hasRefTypes) {
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode> <SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode> <InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode> <DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
<CompatibilityMode>Version8_3_24</CompatibilityMode> <CompatibilityMode>$stubCompatMode</CompatibilityMode>
<DefaultConstantsForm/> <DefaultConstantsForm/>
</Properties> </Properties>
<ChildObjects>$childXml <ChildObjects>$childXml
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build # stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -339,6 +339,66 @@ def scan_ref_types(source_dir):
return type_map return type_map
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_stub_format_version(source_dir):
"""Версия формата заглушечной конфигурации.
Заглушке нужна САМАЯ НИЗКАЯ работающая версия, а не версия исходников: ограничение платформы
одностороннее она читает формат не новее себя. Отсюда min(версия исходников, 2.17): на 2.17+
заглушка остаётся 2.17 (как было), а под исходники 2.13-2.16 опускается до их версии, иначе
конфигурация не загрузится платформой, которая эти исходники и выгрузила («Неизвестная версия
формата 2.17 загружаемого файла», замерено на 8.3.20).
Версию исходников берём из корня собираемого объекта (ExternalDataProcessor/ExternalReport);
вложенные файлы запасной вариант, если корень почему-то не попался.
"""
root_version = ""
any_version = ""
ver_pattern = re.compile(r'<MetaDataObject[^>]+version="(\d+\.\d+)"')
root_pattern = re.compile(r'<(ExternalDataProcessor|ExternalReport)[ >]')
for dirpath, _, filenames in os.walk(source_dir):
for fn in filenames:
if not fn.endswith('.xml'):
continue
try:
with open(os.path.join(dirpath, fn), 'r', encoding='utf-8-sig') as f:
content = f.read()
except Exception:
continue
m = ver_pattern.search(content)
if not m:
continue
if not any_version:
any_version = m.group(1)
if not root_version and root_pattern.search(content):
root_version = m.group(1)
src_version = root_version or any_version or "2.17"
src_rank = format_rank(src_version)
return src_version if 0 < src_rank < format_rank("2.17") else "2.17"
# Режим совместимости заглушки — по той же логике, что и версия формата. Платформа отказывается
# работать с конфигурацией, чей режим выше её самой («Для работы с конфигурацией необходима версия
# платформы не меньше, чем 8.3.24»), и тогда объекты заглушки в базу не попадают: загрузка
# рапортует успех, а сборка падает на «Неизвестное имя типа». Ступени — лестница версий формата
# из docs/1c-configuration-spec.md.
COMPAT_BY_FORMAT = {
"2.13": "Version8_3_20",
"2.14": "Version8_3_21",
"2.15": "Version8_3_22",
"2.16": "Version8_3_23",
}
def stub_compatibility_mode(format_version):
return COMPAT_BY_FORMAT.get(format_version, "Version8_3_24")
def scan_register_columns(source_dir): def scan_register_columns(source_dir):
"""Scan Form.xml for register record set columns referenced via DataPath. """Scan Form.xml for register record set columns referenced via DataPath.
Returns {"RegisterType.RegisterName": {"col1": True, "col2": True}}.""" Returns {"RegisterType.RegisterName": {"col1": True, "col2": True}}."""
@@ -417,7 +477,7 @@ NS = (
'xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" ' 'xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" '
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" ' 'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" ' 'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"' 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
) )
CLASS_IDS = [ CLASS_IDS = [
@@ -1046,6 +1106,9 @@ def main():
type_map = scan_ref_types(args.SourceDir) type_map = scan_ref_types(args.SourceDir)
register_columns = scan_register_columns(args.SourceDir) register_columns = scan_register_columns(args.SourceDir)
has_ref_types = len(type_map) > 0 has_ref_types = len(type_map) > 0
stub_format_version = detect_stub_format_version(args.SourceDir)
stub_compat = stub_compatibility_mode(stub_format_version)
ns_decl = f'{NS} version="{stub_format_version}"'
temp_base = args.TempBasePath or os.path.join(tempfile.gettempdir(), f'epf_stub_db_{random.randint(0,999999)}') temp_base = args.TempBasePath or os.path.join(tempfile.gettempdir(), f'epf_stub_db_{random.randint(0,999999)}')
@@ -1077,7 +1140,7 @@ def main():
child_xml += f'\n\t\t\t<{tag}>{name}</{tag}>' child_xml += f'\n\t\t\t<{tag}>{name}</{tag}>'
cfg_xml = f"""<?xml version="1.0" encoding="UTF-8"?> cfg_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject {NS}> <MetaDataObject {ns_decl}>
\t<Configuration uuid="{uuid_cfg}"> \t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>{co_xml} \t\t<InternalInfo>{co_xml}
\t\t</InternalInfo> \t\t</InternalInfo>
@@ -1086,7 +1149,7 @@ def main():
\t\t\t<Synonym/> \t\t\t<Synonym/>
\t\t\t<Comment/> \t\t\t<Comment/>
\t\t\t<NamePrefix/> \t\t\t<NamePrefix/>
\t\t\t<ConfigurationExtensionCompatibilityMode>Version8_3_24</ConfigurationExtensionCompatibilityMode> \t\t\t<ConfigurationExtensionCompatibilityMode>{stub_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>
@@ -1137,7 +1200,7 @@ def main():
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode> \t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode> \t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode> \t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
\t\t\t<CompatibilityMode>Version8_3_24</CompatibilityMode> \t\t\t<CompatibilityMode>{stub_compat}</CompatibilityMode>
\t\t\t<DefaultConstantsForm/> \t\t\t<DefaultConstantsForm/>
\t\t</Properties> \t\t</Properties>
\t\t<ChildObjects>{child_xml} \t\t<ChildObjects>{child_xml}
@@ -1151,7 +1214,7 @@ def main():
lang_dir = os.path.join(cfg_dir, 'Languages') lang_dir = os.path.join(cfg_dir, 'Languages')
os.makedirs(lang_dir, exist_ok=True) os.makedirs(lang_dir, exist_ok=True)
lang_xml = f"""<?xml version="1.0" encoding="UTF-8"?> lang_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject {NS}> <MetaDataObject {ns_decl}>
\t<Language uuid="{uuid_lang}"> \t<Language uuid="{uuid_lang}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</Name> \t\t\t<Name>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</Name>
@@ -1280,7 +1343,7 @@ def main():
child_obj_xml = '\n\t\t<ChildObjects/>' child_obj_xml = '\n\t\t<ChildObjects/>'
obj_xml = f"""<?xml version="1.0" encoding="UTF-8"?> obj_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject {NS}> <MetaDataObject {ns_decl}>
\t<{tag} uuid="{obj_uuid}">{internal_xml} \t<{tag} uuid="{obj_uuid}">{internal_xml}
\t\t<Properties> \t\t<Properties>
{props_xml} {props_xml}
+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")
+14 -7
View File
@@ -18,19 +18,26 @@ 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` | Версия формата выгрузки — см. ниже |
`FormatVersion`**не выше** версии формата платформы, на которой объект будут собирать и открывать:
8.3.24 — `2.17`, 8.3.25 — `2.18`, 8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно:
платформа читает свой формат и любой более старый, поэтому дефолт `2.17` подходит для всей линейки
8.3.24 и выше. Для более старых платформ счёт идёт так же, по одной версии на релиз (8.3.23 — `2.16`),
но на них навыки не проверялись — такое значение принимается с предупреждением.
## Команда ## Команда
```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 "<версия>"]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
+64 -6
View File
@@ -1,4 +1,4 @@
# epf-init v1.1 — Init 1C external data processor scaffold # epf-init v1.8 — 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,21 +6,65 @@ param(
[string]$Synonym = $Name, [string]$Synonym = $Name,
[string]$SrcDir = "src" [string]$SrcDir = "src",
# Версия формата выгрузки. Своей конфигурации у автономной обработки нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри обработки
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
[string]$FormatVersion = "2.17"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# --- Format version ---
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
# на нечисловое значение: это опечатка, а не версия.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$formatRank = Get-FormatRank $FormatVersion
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
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
if ($formatRank -eq 0) {
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
exit 1
}
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
}
$uuid1 = [guid]::NewGuid().ToString() $uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString() $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 ($formatRank -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 +77,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 +108,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 +136,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"
+104 -10
View File
@@ -1,19 +1,65 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-init v1.1 — Init 1C external data processor scaffold # epf-init v1.8 — 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
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
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 +67,24 @@ 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')
args = ci_parse_args(parser)
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
format_rank_value = format_rank(args.FormatVersion)
if format_rank_value == 0:
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
sys.exit(1)
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
f"but was not verified on that platform", file=sys.stderr)
name = args.Name name = args.Name
synonym = args.Synonym if args.Synonym else name synonym = args.Synonym if args.Synonym else name
@@ -32,8 +95,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 +137,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 +163,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 +180,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.5 — 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(
@@ -111,6 +111,19 @@ $finalize = {
} }
} }
# --- Format version ---
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Reference tables --- # --- Reference tables ---
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' $guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
@@ -183,11 +196,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
} }
$version = $root.GetAttribute("version") $version = $root.GetAttribute("version")
$versionRank = Get-FormatRank $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", "2.21")) { } elseif ($versionRank -eq 0) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). Report-Error "1. Malformed version '$version' (expected N.N)"
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)" } elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} }
# Detect type: ExternalDataProcessor or ExternalReport # Detect type: ExternalDataProcessor or ExternalReport
@@ -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.5 — 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"
@@ -38,6 +60,21 @@ CHILD_TYPE_ORDER = {
} }
# ── Format version ───────────────────────────────────────────
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
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 localname(el): def localname(el):
return etree.QName(el.tag).localname return etree.QName(el.tag).localname
@@ -50,7 +87,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
@@ -163,11 +200,17 @@ def main():
check1_ok = False check1_ok = False
version = root.get("version", "") version = root.get("version", "")
version_rank = format_rank(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", "2.21"): elif version_rank == 0:
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). report_error(f"1. Malformed version '{version}' (expected N.N)")
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)") elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
report_warn(f"1. Format version '{version}' is below the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
report_warn(f"1. Format version '{version}' is above the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
# Detect type # Detect type
child_elements = [] child_elements = []
+15 -8
View File
@@ -18,20 +18,27 @@ 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` | Версия формата выгрузки — см. ниже |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
`FormatVersion`**не выше** версии формата платформы, на которой объект будут собирать и открывать:
8.3.24 — `2.17`, 8.3.25 — `2.18`, 8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно:
платформа читает свой формат и любой более старый, поэтому дефолт `2.17` подходит для всей линейки
8.3.24 и выше. Для более старых платформ счёт идёт так же, по одной версии на релиз (8.3.23 — `2.16`),
но на них навыки не проверялись — такое значение принимается с предупреждением.
## Команда ## Команда
```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 "<версия>"] [-WithSKD]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
+67 -9
View File
@@ -1,4 +1,4 @@
# erf-init v1.1 — Init 1C external report scaffold # erf-init v1.8 — 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,18 +8,62 @@ param(
[string]$SrcDir = "src", [string]$SrcDir = "src",
[switch]$WithSKD [switch]$WithSKD,
# Версия формата выгрузки. Своей конфигурации у автономного отчёта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри отчёта
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
[string]$FormatVersion = "2.17"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# --- Format version ---
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
# на нечисловое значение: это опечатка, а не версия.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$formatRank = Get-FormatRank $FormatVersion
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
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
if ($formatRank -eq 0) {
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
exit 1
}
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
}
$uuid1 = [guid]::NewGuid().ToString() $uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString() $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 ($formatRank -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 +92,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 +105,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 +142,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 +170,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 +194,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 +211,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 +231,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"
+107 -13
View File
@@ -1,19 +1,65 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# erf-init v1.1 — Init 1C external report scaffold # erf-init v1.8 — 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
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
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 +67,25 @@ 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')
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)
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
format_rank_value = format_rank(args.FormatVersion)
if format_rank_value == 0:
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
sys.exit(1)
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
f"but was not verified on that platform", file=sys.stderr)
name = args.Name name = args.Name
synonym = args.Synonym if args.Synonym else name synonym = args.Synonym if args.Synonym else name
@@ -33,6 +96,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 +136,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 +149,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 +181,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 +198,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 +218,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 +234,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 +252,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.192 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
# 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>"
} }
} }
@@ -5871,9 +5926,17 @@ function Emit-Attributes {
} }
if ($hasAddCols) { if ($hasAddCols) {
foreach ($ac in @($attr.additionalColumns)) { foreach ($ac in @($attr.additionalColumns)) {
# Пустой список колонок задаётся ЯВНО (`"columns": []`) — это законная форма,
# платформа так пишет таблицу, у которой доп. колонок нет. А вот отсутствие ключа
# — недосказанность автора: «доп. колонки есть», а какие, не указано. Раньше на
# этом PS падал с «Не удается индексировать в массив NULL» (@($null).Count = 1).
if ($null -eq $ac.PSObject.Properties['columns'] -or $null -eq $ac.columns) {
Write-Error "additionalColumns group for table '$($ac.table)': key 'columns' is missing — list the columns, or pass an empty array for a table without extra columns"
exit 1
}
$acCols = @($ac.columns) $acCols = @($ac.columns)
if ($acCols.Count -eq 0) { if ($acCols.Count -eq 0) {
# Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа) # Явно пустая группа → self-closing (как платформа)
X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>" X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>"
continue continue
} }
@@ -5908,7 +5971,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 +5986,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 +5997,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 +6017,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 +6155,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 +6247,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 +6526,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 +6587,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 +6674,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 +6729,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.149 — 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.149 — 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"
@@ -81,29 +103,29 @@ def _attr(node, name, ns_uri=None):
def convert_string_to_json_literal(s): def convert_string_to_json_literal(s):
if s is None: if s is None:
return 'null' return 'null'
sb = ['"'] out = ['"']
for ch in s: for ch in s:
code = ord(ch) code = ord(ch)
if code == 0x22: if code == 0x22:
sb.append('\\"') out.append('\\"')
elif code == 0x5C: elif code == 0x5C:
sb.append('\\\\') out.append('\\\\')
elif code == 0x08: elif code == 0x08:
sb.append('\\b') out.append('\\b')
elif code == 0x09: elif code == 0x09:
sb.append('\\t') out.append('\\t')
elif code == 0x0A: elif code == 0x0A:
sb.append('\\n') out.append('\\n')
elif code == 0x0C: elif code == 0x0C:
sb.append('\\f') out.append('\\f')
elif code == 0x0D: elif code == 0x0D:
sb.append('\\r') out.append('\\r')
elif code < 0x20: elif code < 0x20:
sb.append('\\u%04x' % code) out.append('\\u%04x' % code)
else: else:
sb.append(ch) out.append(ch)
sb.append('"') out.append('"')
return ''.join(sb) return ''.join(out)
def _num_to_str(obj): def _num_to_str(obj):
@@ -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.17 — 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)]
@@ -56,9 +56,23 @@ try {
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") $nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") $nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
$root = $xmlDoc.DocumentElement $root = $xmlDoc.DocumentElement
# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья
# support-guard: is_external_root, авторитет — cf-edit).
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
# --- Detect context: config vs EPF/ERF --- # --- Detect context: config vs EPF/ERF ---
# Walk up from FormPath looking for Configuration.xml → config context # Walk up from FormPath looking for Configuration.xml → config context
# No Configuration.xml → external data processor / report (EPF/ERF) # No Configuration.xml → external data processor / report (EPF/ERF)
@@ -66,13 +80,56 @@ $script:isConfigContext = $false
$walkDir = Split-Path (Resolve-Path $FormPath) -Parent $walkDir = Split-Path (Resolve-Path $FormPath) -Parent
for ($i = 0; $i -lt 15; $i++) { for ($i = 0; $i -lt 15; $i++) {
if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break } if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break }
# Порядок проверок тот же, что у Detect-FormatVersion: сначала корень автономной обработки,
# потом Configuration.xml — иначе форма внутри EPF, лежащей в дереве конфигурации, взяла бы
# версию конфигурации.
$extRoot = "$walkDir.xml"
if (-not $script:versionAnchor) {
if (Test-ExternalObjectRoot $extRoot) {
# Ближайший якорь побеждает: автономная обработка остаётся автономной, даже если её
# исходники лежат внутри дерева с Configuration.xml (типовая раскладка проекта:
# src/cf рядом с src/epf). Иначе её собственные External*-типы считались бы ошибкой.
$script:versionAnchor = $extRoot
break
}
}
if (Test-Path (Join-Path $walkDir "Configuration.xml")) { if (Test-Path (Join-Path $walkDir "Configuration.xml")) {
$script:isConfigContext = $true $script:isConfigContext = $true
$script:configXmlPath = Join-Path $walkDir "Configuration.xml"
if (-not $script:versionAnchor) { $script:versionAnchor = $script:configXmlPath }
break break
} }
$walkDir = Split-Path $walkDir $walkDir = Split-Path $walkDir
} }
# Версия формата выгрузки. Копия общего эталона (семья detect_format_version, авторитет —
# form-compile): та же ветка для автономной EPF/ERF, где версию несёт корень обработки.
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"
}
# --- Counters --- # --- Counters ---
$errors = 0 $errors = 0
@@ -101,6 +158,19 @@ function Report-Warn {
Write-Host "[WARN] $msg" Write-Host "[WARN] $msg"
} }
# --- Format version ---
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Form name from path --- # --- Form name from path ---
$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath) $formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath)
@@ -127,13 +197,17 @@ if ($root.LocalName -ne "Form") {
Report-Error "Root element is '$($root.LocalName)', expected 'Form'" Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
} else { } else {
$version = $root.GetAttribute("version") $version = $root.GetAttribute("version")
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). $versionRank = Get-FormatRank $version
if ($version -in @("2.17", "2.18", "2.19", "2.20")) { if (-not $version) {
Report-OK "Root element: Form version=$version"
} elseif ($version) {
Report-Warn "Form version='$version' (expected 2.17-2.20)"
} else {
Report-Warn "Form version attribute missing" Report-Warn "Form version attribute missing"
} elseif ($versionRank -eq 0) {
Report-Error "Malformed version '$version' (expected N.N)"
} elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
Report-Warn "Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
Report-Warn "Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} else {
Report-OK "Root element: Form version=$version"
} }
} }
@@ -144,10 +218,15 @@ if (-not $stopped) {
if ($acb) { if ($acb) {
$acbName = $acb.GetAttribute("name") $acbName = $acb.GetAttribute("name")
$acbId = $acb.GetAttribute("id") $acbId = $acb.GetAttribute("id")
# id=-1 — соглашение, а не требование: в корпусе УТ/БП/ERP так у 21 094 форм из 21 097,
# но три формы платформа выгружает с обычным id и грузит их без нареканий. Поэтому
# предупреждение; ошибка — только если id вовсе не число.
if ($acbId -eq "-1") { if ($acbId -eq "-1") {
Report-OK "AutoCommandBar: name='$acbName', id=$acbId" Report-OK "AutoCommandBar: name='$acbName', id=$acbId"
} elseif ($acbId -match '^-?\d+$') {
Report-Warn "AutoCommandBar id='$acbId', usually '-1'"
} else { } else {
Report-Error "AutoCommandBar id='$acbId', expected '-1'" Report-Error "AutoCommandBar id='$acbId' is not a number"
} }
} else { } else {
Report-Error "AutoCommandBar element missing" Report-Error "AutoCommandBar element missing"
@@ -427,11 +506,19 @@ if (-not $stopped) {
$segments = $cleanPath -split '\.' $segments = $cleanPath -split '\.'
$rootAttr = $segments[0] $rootAttr = $segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute # Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute.
if ($rootAttr -eq 'Items') { # Разрешаем ЦЕПОЧКОЙ: таблица во вложенной таблице сама привязана через Items.*, и один
# шаг оставлял корнем литерал «Items» — форма платформы объявлялась битой (типовые
# НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP).
$itemsHops = 0
$itemsBroken = $false
while ($rootAttr -eq 'Items') {
$itemsHops++
if ($itemsHops -gt 10) { $itemsBroken = $true; break } # страховка от кольца ссылок
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') { if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*" Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
continue $itemsBroken = $true
break
} }
$tableName = $segments[1] $tableName = $segments[1]
$tableEl = $null $tableEl = $null
@@ -444,17 +531,21 @@ if (-not $stopped) {
if (-not $tableEl) { if (-not $tableEl) {
Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found" Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found"
$pathErrors++ $pathErrors++
continue $itemsBroken = $true
break
} }
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr) $tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) { if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
# Table without DataPath — can't resolve further, accept silently # Table without DataPath — can't resolve further, accept silently
continue $itemsBroken = $true
break
} }
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', '' $tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) } if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
$rootAttr = ($tableDp -split '\.')[0] $segments = $tableDp -split '\.'
$rootAttr = $segments[0]
} }
if ($itemsBroken) { continue }
if (-not $attrMap.ContainsKey($rootAttr)) { if (-not $attrMap.ContainsKey($rootAttr)) {
Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found" Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found"
@@ -569,13 +660,18 @@ if (-not $stopped) {
$actionErrors = 0 $actionErrors = 0
$actionChecked = 0 $actionChecked = 0
# Предупреждение, а не ошибка: <Action> может назначаться в рантайме
# (`Команда.Действие = "Подключаемый_…"` в ПриСозданииНаСервере) — приём типовых конфигураций
# там, где обработчик существует не во всякой сборке. Назначать может и чужой модуль
# (переопределяемый слой, подключаемые команды), так что по одному Form.xml не решить.
# Корпус УТ/БП/ERP: 406 таких команд на 275 формах, произведённых платформой.
foreach ($cmd in $cmdNodes) { foreach ($cmd in $cmdNodes) {
if ($stopped) { break } if ($stopped) { break }
$cmdName = $cmd.GetAttribute("name") $cmdName = $cmd.GetAttribute("name")
$actionNode = $cmd.SelectSingleNode("f:Action", $nsMgr) $actionNode = $cmd.SelectSingleNode("f:Action", $nsMgr)
$actionChecked++ $actionChecked++
if (-not $actionNode -or -not $actionNode.InnerText.Trim()) { if (-not $actionNode -or -not $actionNode.InnerText.Trim()) {
Report-Error "Command '$cmdName': missing or empty Action" Report-Warn "Command '$cmdName': no Action — handler must be assigned at runtime, otherwise the command does nothing"
$actionErrors++ $actionErrors++
} }
} }
@@ -742,6 +838,41 @@ if (-not $stopped -and $isExtension) {
Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000" Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000"
} }
} }
# 11d. Пути на основной реквизит, которого форма не объявляет.
# Check 5 такое пропускает: у заимствованной формы он не проверяет базовые элементы (id < 1000000),
# а привязки в <xr:Link> вообще вне его списка тегов. Между тем это ровно тот случай, на котором
# платформа отвергает загрузку: «Неверный путь к полю - Объект.X». Правило: если основной реквизит
# не объявлен в <Attributes> формы, любой путь с его корнем не разрешится.
# Корень берётся из основного реквизита BaseForm: «Объект» он только у формы объекта, у формы
# списка это «Список», у формы записи регистра «Запись». С зашитым «Объект» проверка на таких
# формах молча не срабатывала — валидатор рапортовал «чисто» на форме, которую платформа не примет.
$mainAttrDeclared = $false
foreach ($attr in $attrNodes) {
$maNode = $attr.SelectSingleNode("f:MainAttribute", $nsMgr)
if ($maNode -and $maNode.InnerText.Trim() -eq "true") { $mainAttrDeclared = $true; break }
}
if (-not $mainAttrDeclared) {
# Значения привязок ищем текстом: интересуют и обычные теги, и <xr:DataPath> внутри
# <ChoiceParameterLinks>, а те живут в чужом пространстве имён.
$rawForm = [System.IO.File]::ReadAllText($FormPath, [System.Text.Encoding]::UTF8)
$mainBase = $baseFormNode.SelectSingleNode("f:Attributes/f:Attribute[f:MainAttribute='true']", $bfNs)
$rootName = if ($mainBase -and $mainBase.GetAttribute("name")) { $mainBase.GetAttribute("name") } else { "Объект" }
$rootPat = [regex]::Escape($rootName)
$danglingPaths = @{}
foreach ($m in [regex]::Matches($rawForm, "<(?:\w+:)?\w*DataPath[^>]*>(${rootPat}\.[^<]+)</(?:\w+:)?\w*DataPath>")) {
$danglingPaths[$m.Groups[1].Value] = $true
}
if ($danglingPaths.Count -gt 0) {
$shown = @($danglingPaths.Keys | Sort-Object)
$sample = ($shown | Select-Object -First 3) -join ", "
$suffix = if ($shown.Count -gt 3) { " (и ещё $($shown.Count - 3))" } else { "" }
Report-Error "Path(s) rooted at '${rootName}' but the form declares no MainAttribute: $sample$suffix"
} elseif ($mainBase) {
Report-OK "Object paths: none dangling (MainAttribute not declared)"
}
}
} }
# Check callType without BaseForm (structural warning) # Check callType without BaseForm (structural warning)
@@ -844,6 +975,71 @@ if (-not $stopped) {
} }
} }
# --- Check 13: префиксы в значениях объявлены в самом файле ---
# `cfg:DataProcessorObject.X` в <v8:Type> при незадекларированном xmlns:cfg — валидный XML, который
# платформа не читает вовсе: «Исключение XDTO произошло при чтении файла». Ошибка типична для
# рукописного XML: префикс скопирован из чужой формы, а объявление в корне забыто. Область видимости
# считаем по узлу (GetNamespaceOfPrefix), а не по корню: локальная xmlns на элементе законна.
if (-not $stopped) {
$prefixErrors = 0
$prefixChecked = 0
$prefixPattern = '^([A-Za-z_][A-Za-z0-9_.-]*):.+$'
# Значения, где префикс обязан резолвиться: тип реквизита/колонки и xsi:type
# Только листовые узлы: под local-name()='Type' подходит и обёртка <Type>, и вложенный <v8:Type>,
# а InnerText обёртки — то же значение, иначе одна ошибка сообщалась бы дважды.
foreach ($node in $xmlDoc.SelectNodes("//*[local-name()='Type' or local-name()='TypeSet']", $nsMgr)) {
if ($node.SelectSingleNode("*")) { continue }
$val = $node.InnerText.Trim()
if (-not $val) { continue }
$m = [regex]::Match($val, $prefixPattern)
if (-not $m.Success) { continue }
$prefixChecked++
$pfx = $m.Groups[1].Value
if (-not $node.GetNamespaceOfPrefix($pfx)) {
Report-Error "13. Type '$val': namespace prefix '${pfx}:' is not declared — the platform cannot read the file (XDTO)"
$prefixErrors++
}
}
foreach ($node in $xmlDoc.SelectNodes("//*[@xsi:type]", $nsMgr)) {
$val = $node.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
$m = [regex]::Match($val, $prefixPattern)
if (-not $m.Success) { continue }
$prefixChecked++
$pfx = $m.Groups[1].Value
if (-not $node.GetNamespaceOfPrefix($pfx)) {
Report-Error "13. xsi:type='$val': namespace prefix '${pfx}:' is not declared — the platform cannot read the file (XDTO)"
$prefixErrors++
}
}
if ($prefixChecked -eq 0) {
Report-OK "13. Namespace prefixes: nothing to check"
} elseif ($prefixErrors -eq 0) {
Report-OK "13. Namespace prefixes: $prefixChecked values, all declared"
}
}
# --- Check 14: версия формата формы совпадает с версией выгрузки ---
# Версию задаёт платформа, которой выгружали, и в пределах одной выгрузки она едина. Форма из
# другой версии — «Неизвестная версия формата N загружаемого файла»: платформа не читает файл,
# который новее её самой. Источник версии ищем общим helper-ом: он же покрывает автономную
# внешнюю обработку/отчёт, где Configuration.xml нет и версию несёт корень самой обработки.
if (-not $stopped -and $script:versionAnchor) {
$formVer = $root.GetAttribute("version")
$dumpVer = Detect-FormatVersion (Split-Path (Resolve-Path $FormPath) -Parent)
if (-not $formVer) {
Report-OK "14. Format version: not comparable"
} elseif ($formVer -ne $dumpVer) {
Report-Error "14. Format version $formVer differs from the dump ($dumpVer) — a dump carries one version, the platform refuses a file it cannot read"
} else {
Report-OK "14. Format version: $formVer, matches the dump"
}
}
# --- Summary --- # --- Summary ---
$checks = $script:okCount + $errors + $warnings $checks = $script:okCount + $errors + $warnings
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-validate v1.9 — Validate 1C managed form # form-validate v1.17 — 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"
@@ -49,6 +71,64 @@ VALID_CFG_PREFIXES = {
} }
# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья
# support-guard: is_external_root, авторитет — cf-edit).
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
# Версия формата выгрузки. Копия общего эталона (семья detect_format_version, авторитет —
# form-compile): та же ветка для автономной EPF/ERF, где версию несёт корень обработки.
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"
# ── Format version ───────────────────────────────────────────
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
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 localname(el): def localname(el):
return etree.QName(el.tag).localname return etree.QName(el.tag).localname
@@ -60,7 +140,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
@@ -106,13 +186,29 @@ def main():
# Detect context: config vs EPF/ERF # Detect context: config vs EPF/ERF
is_config_context = False is_config_context = False
config_xml_path = ''
version_anchor = ''
walk_dir = os.path.dirname(os.path.abspath(form_path)) walk_dir = os.path.dirname(os.path.abspath(form_path))
for _ in range(15): for _ in range(15):
parent = os.path.dirname(walk_dir) parent = os.path.dirname(walk_dir)
if parent == walk_dir: if parent == walk_dir:
break break
# Порядок проверок тот же, что у detect_format_version: сначала корень автономной обработки,
# потом Configuration.xml — иначе форма внутри EPF, лежащей в дереве конфигурации, взяла бы
# версию конфигурации.
ext_root = walk_dir + '.xml'
if not version_anchor:
if _sg_is_external_root(ext_root):
# Ближайший якорь побеждает: автономная обработка остаётся автономной, даже если её
# исходники лежат внутри дерева с Configuration.xml (типовая раскладка проекта:
# src/cf рядом с src/epf). Иначе её собственные External*-типы считались бы ошибкой.
version_anchor = ext_root
break
if os.path.isfile(os.path.join(walk_dir, 'Configuration.xml')): if os.path.isfile(os.path.join(walk_dir, 'Configuration.xml')):
is_config_context = True is_config_context = True
config_xml_path = os.path.join(walk_dir, 'Configuration.xml')
if not version_anchor:
version_anchor = config_xml_path
break break
walk_dir = parent walk_dir = parent
@@ -161,13 +257,19 @@ def main():
report_error(f"Root element is '{localname(root)}', expected 'Form'") report_error(f"Root element is '{localname(root)}', expected 'Form'")
else: else:
version = root.get("version", "") version = root.get("version", "")
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). version_rank = format_rank(version)
if version in ("2.17", "2.18", "2.19", "2.20"): if not version:
report_ok(f"Root element: Form version={version}")
elif version:
report_warn(f"Form version='{version}' (expected 2.17-2.20)")
else:
report_warn("Form version attribute missing") report_warn("Form version attribute missing")
elif version_rank == 0:
report_error(f"Malformed version '{version}' (expected N.N)")
elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
report_warn(f"Format version '{version}' is below the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
report_warn(f"Format version '{version}' is above the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
else:
report_ok(f"Root element: Form version={version}")
# --- Check 2: AutoCommandBar --- # --- Check 2: AutoCommandBar ---
if not stopped: if not stopped:
@@ -175,10 +277,15 @@ def main():
if acb is not None: if acb is not None:
acb_name = acb.get("name", "") acb_name = acb.get("name", "")
acb_id = acb.get("id", "") acb_id = acb.get("id", "")
# id=-1 — соглашение, а не требование: в корпусе УТ/БП/ERP так у 21 094 форм из 21 097,
# но три формы платформа выгружает с обычным id и грузит их без нареканий. Поэтому
# предупреждение; ошибка — только если id вовсе не число.
if acb_id == "-1": if acb_id == "-1":
report_ok(f"AutoCommandBar: name='{acb_name}', id={acb_id}") report_ok(f"AutoCommandBar: name='{acb_name}', id={acb_id}")
elif re.match(r'^-?\d+$', acb_id):
report_warn(f"AutoCommandBar id='{acb_id}', usually '-1'")
else: else:
report_error(f"AutoCommandBar id='{acb_id}', expected '-1'") report_error(f"AutoCommandBar id='{acb_id}' is not a number")
else: else:
report_error("AutoCommandBar element missing") report_error("AutoCommandBar element missing")
@@ -430,11 +537,21 @@ def main():
segments = clean_path.split(".") segments = clean_path.split(".")
root_attr = segments[0] root_attr = segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute # Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute.
if root_attr == 'Items': # Разрешаем ЦЕПОЧКОЙ: таблица во вложенной таблице сама привязана через Items.*, и один
# шаг оставлял корнем литерал «Items» — форма платформы объявлялась битой (типовые
# НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP).
items_hops = 0
items_broken = False
while root_attr == 'Items':
items_hops += 1
if items_hops > 10: # страховка от кольца ссылок
items_broken = True
break
if len(segments) < 3 or segments[2] != 'CurrentData': if len(segments) < 3 or segments[2] != 'CurrentData':
report_warn(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — unknown Items.* shape, expected Items.<Table>.CurrentData.*") report_warn(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — unknown Items.* shape, expected Items.<Table>.CurrentData.*")
continue items_broken = True
break
table_name = segments[1] table_name = segments[1]
table_el = None table_el = None
for candidate in all_elements: for candidate in all_elements:
@@ -444,14 +561,19 @@ def main():
if table_el is None: if table_el is None:
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — table element '{table_name}' not found") report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — table element '{table_name}' not found")
path_errors += 1 path_errors += 1
continue items_broken = True
break
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath") table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
if table_dp_node is None or not (table_dp_node.text or "").strip(): if table_dp_node is None or not (table_dp_node.text or "").strip():
continue items_broken = True
break
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip()) table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
if table_dp.startswith('~'): if table_dp.startswith('~'):
table_dp = table_dp[1:] table_dp = table_dp[1:]
root_attr = table_dp.split(".")[0] segments = table_dp.split(".")
root_attr = segments[0]
if items_broken:
continue
if root_attr not in attr_map: if root_attr not in attr_map:
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — attribute '{root_attr}' not found") report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — attribute '{root_attr}' not found")
@@ -465,6 +587,8 @@ def main():
path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note
if path_errors == 0 and path_msg: if path_errors == 0 and path_msg:
report_ok(f"Data bindings: {path_msg}") report_ok(f"Data bindings: {path_msg}")
elif path_errors == 0:
report_ok("Data bindings: none")
# --- Check 6: Button command references --- # --- Check 6: Button command references ---
if not stopped: if not stopped:
@@ -499,6 +623,8 @@ def main():
if cmd_errors == 0 and cmd_checked > 0: if cmd_errors == 0 and cmd_checked > 0:
report_ok(f"Command references: {cmd_checked} buttons checked") report_ok(f"Command references: {cmd_checked} buttons checked")
elif cmd_checked == 0:
report_ok("Command references: none")
# --- Check 7: Events have handler names --- # --- Check 7: Events have handler names ---
if not stopped: if not stopped:
@@ -538,12 +664,19 @@ def main():
if event_errors == 0 and event_checked > 0: if event_errors == 0 and event_checked > 0:
report_ok(f"Event handlers: {event_checked} events checked") report_ok(f"Event handlers: {event_checked} events checked")
elif event_checked == 0:
report_ok("Event handlers: none")
# --- Check 8: Command actions --- # --- Check 8: Command actions ---
if not stopped: if not stopped:
action_errors = 0 action_errors = 0
action_checked = 0 action_checked = 0
# Предупреждение, а не ошибка: <Action> может назначаться в рантайме
# (`Команда.Действие = "Подключаемый_…"` в ПриСозданииНаСервере) — приём типовых конфигураций
# там, где обработчик существует не во всякой сборке. Назначать может и чужой модуль
# (переопределяемый слой, подключаемые команды), так что по одному Form.xml не решить.
# Корпус УТ/БП/ERP: 406 таких команд на 275 формах, произведённых платформой.
for cmd in cmd_nodes: for cmd in cmd_nodes:
if stopped: if stopped:
break break
@@ -551,11 +684,13 @@ def main():
action_node = cmd.find(f"{{{F_NS}}}Action") action_node = cmd.find(f"{{{F_NS}}}Action")
action_checked += 1 action_checked += 1
if action_node is None or not (action_node.text or "").strip(): if action_node is None or not (action_node.text or "").strip():
report_error(f"Command '{cmd_name}': missing or empty Action") report_warn(f"Command '{cmd_name}': no Action — handler must be assigned at runtime, otherwise the command does nothing")
action_errors += 1 action_errors += 1
if action_errors == 0 and action_checked > 0: if action_errors == 0 and action_checked > 0:
report_ok(f"Command actions: {action_checked} commands checked") report_ok(f"Command actions: {action_checked} commands checked")
elif action_checked == 0:
report_ok("Command actions: none")
# --- Check 9: MainAttribute count --- # --- Check 9: MainAttribute count ---
if not stopped: if not stopped:
@@ -686,6 +821,39 @@ def main():
if (ext_attr_count + ext_cmd_count) > 0: if (ext_attr_count + ext_cmd_count) > 0:
report_ok(f"Extension ID ranges: {ext_attr_count} attr(s), {ext_cmd_count} cmd(s) \u2014 all >= 1000000") report_ok(f"Extension ID ranges: {ext_attr_count} attr(s), {ext_cmd_count} cmd(s) \u2014 all >= 1000000")
# 11d. \u041f\u0443\u0442\u0438 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442, \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0430 \u043d\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u044f\u0435\u0442.
# Check 5 \u0442\u0430\u043a\u043e\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442: \u0443 \u0437\u0430\u0438\u043c\u0441\u0442\u0432\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u0444\u043e\u0440\u043c\u044b \u043e\u043d \u043d\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0431\u0430\u0437\u043e\u0432\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b (id < 1000000),
# \u0430 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u0432 <xr:Link> \u0432\u043e\u043e\u0431\u0449\u0435 \u0432\u043d\u0435 \u0435\u0433\u043e \u0441\u043f\u0438\u0441\u043a\u0430 \u0442\u0435\u0433\u043e\u0432. \u041c\u0435\u0436\u0434\u0443 \u0442\u0435\u043c \u044d\u0442\u043e \u0440\u043e\u0432\u043d\u043e \u0442\u043e\u0442 \u0441\u043b\u0443\u0447\u0430\u0439, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c
# \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u043e\u0442\u0432\u0435\u0440\u0433\u0430\u0435\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443: \u00ab\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043f\u043e\u043b\u044e - \u041e\u0431\u044a\u0435\u043a\u0442.X\u00bb. \u041f\u0440\u0430\u0432\u0438\u043b\u043e: \u0435\u0441\u043b\u0438 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442
# \u043d\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d \u0432 <Attributes> \u0444\u043e\u0440\u043c\u044b, \u043b\u044e\u0431\u043e\u0439 \u043f\u0443\u0442\u044c \u0441 \u0435\u0433\u043e \u043a\u043e\u0440\u043d\u0435\u043c \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0441\u044f.
# \u041a\u043e\u0440\u0435\u043d\u044c \u0431\u0435\u0440\u0451\u0442\u0441\u044f \u0438\u0437 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430 BaseForm: \u00ab\u041e\u0431\u044a\u0435\u043a\u0442\u00bb \u043e\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u0443 \u0444\u043e\u0440\u043c\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430, \u0443 \u0444\u043e\u0440\u043c\u044b
# \u0441\u043f\u0438\u0441\u043a\u0430 \u044d\u0442\u043e \u00ab\u0421\u043f\u0438\u0441\u043e\u043a\u00bb, \u0443 \u0444\u043e\u0440\u043c\u044b \u0437\u0430\u043f\u0438\u0441\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430 \u00ab\u0417\u0430\u043f\u0438\u0441\u044c\u00bb. \u0421 \u0437\u0430\u0448\u0438\u0442\u044b\u043c \u00ab\u041e\u0431\u044a\u0435\u043a\u0442\u00bb \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u0442\u0430\u043a\u0438\u0445
# \u0444\u043e\u0440\u043c\u0430\u0445 \u043c\u043e\u043b\u0447\u0430 \u043d\u0435 \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043b\u0430.
main_attr_declared = False
for attr in attr_nodes:
ma_node = attr.find(f"{{{F_NS}}}MainAttribute")
if ma_node is not None and (ma_node.text or "").strip() == "true":
main_attr_declared = True
break
if not main_attr_declared:
# \u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u0440\u0438\u0432\u044f\u0437\u043e\u043a \u0438\u0449\u0435\u043c \u0442\u0435\u043a\u0441\u0442\u043e\u043c: \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0443\u044e\u0442 \u0438 \u043e\u0431\u044b\u0447\u043d\u044b\u0435 \u0442\u0435\u0433\u0438, \u0438 <xr:DataPath> \u0432\u043d\u0443\u0442\u0440\u0438
# <ChoiceParameterLinks>, \u0430 \u0442\u0435 \u0436\u0438\u0432\u0443\u0442 \u0432 \u0447\u0443\u0436\u043e\u043c \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0435 \u0438\u043c\u0451\u043d.
with open(form_path, "r", encoding="utf-8-sig") as fh:
raw_form = fh.read()
main_base = base_form_node.find(f"{{{F_NS}}}Attributes/{{{F_NS}}}Attribute[{{{F_NS}}}MainAttribute='true']")
root_name = main_base.get("name") if main_base is not None and main_base.get("name") else "\u041e\u0431\u044a\u0435\u043a\u0442"
root_pat = re.escape(root_name)
dangling_paths = set(re.findall(
r'<(?:\w+:)?\w*DataPath[^>]*>(' + root_pat + r'\.[^<]+)</(?:\w+:)?\w*DataPath>', raw_form))
if dangling_paths:
shown = sorted(dangling_paths)
sample = ", ".join(shown[:3])
suffix = f" (\u0438 \u0435\u0449\u0451 {len(shown) - 3})" if len(shown) > 3 else ""
report_error(f"Path(s) rooted at '{root_name}' but the form declares no MainAttribute: {sample}{suffix}")
elif main_base is not None:
report_ok("Object paths: none dangling (MainAttribute not declared)")
# Check callType without BaseForm # Check callType without BaseForm
if not stopped and not is_extension: if not stopped and not is_extension:
call_type_without_base = False call_type_without_base = False
@@ -748,6 +916,62 @@ def main():
else: else:
report_ok('12. Types: no type values to check') report_ok('12. Types: no type values to check')
# --- Check 13: префиксы в значениях объявлены в самом файле ---
# `cfg:DataProcessorObject.X` в <v8:Type> при незадекларированном xmlns:cfg — валидный XML, который
# платформа не читает вовсе: «Исключение XDTO произошло при чтении файла». Ошибка типична для
# рукописного XML: префикс скопирован из чужой формы, а объявление в корне забыто. Область видимости
# считаем по узлу (nsmap элемента), а не по корню: локальная xmlns на элементе законна.
if not stopped:
prefix_errors = 0
prefix_checked = 0
prefix_re = re.compile(r'^([A-Za-z_][A-Za-z0-9_.-]*):.+$')
for node in root.iter():
if not isinstance(node.tag, str):
continue
ln = localname(node)
values = []
if ln in ('Type', 'TypeSet'):
values.append((node.text or '').strip())
xsi_type = node.get(f'{{{"http://www.w3.org/2001/XMLSchema-instance"}}}type')
if xsi_type:
values.append(xsi_type.strip())
for val in values:
if not val:
continue
m = prefix_re.match(val)
if not m:
continue
prefix_checked += 1
pfx = m.group(1)
if pfx not in node.nsmap:
kind = "xsi:type" if val == xsi_type else "Type"
report_error(f"13. {kind} '{val}': namespace prefix '{pfx}:' is not declared "
"— the platform cannot read the file (XDTO)")
prefix_errors += 1
if prefix_checked == 0:
report_ok('13. Namespace prefixes: nothing to check')
elif prefix_errors == 0:
report_ok(f'13. Namespace prefixes: {prefix_checked} values, all declared')
# --- Check 14: версия формата формы совпадает с версией выгрузки ---
# Версию задаёт платформа, которой выгружали, и в пределах одной выгрузки она едина. Форма из
# другой версии — «Неизвестная версия формата N загружаемого файла»: платформа не читает файл,
# который новее её самой. Источник версии ищем общим helper-ом: он же покрывает автономную
# внешнюю обработку/отчёт, где Configuration.xml нет и версию несёт корень самой обработки.
if not stopped and version_anchor:
form_ver = root.get('version', '')
dump_ver = detect_format_version(os.path.dirname(os.path.abspath(form_path)))
if not form_ver:
report_ok('14. Format version: not comparable')
elif form_ver != dump_ver:
report_error(f'14. Format version {form_ver} differs from the dump ({dump_ver}) '
'— a dump carries one version, the platform refuses a file it cannot read')
else:
report_ok(f'14. Format version: {form_ver}, matches the dump')
# --- Finalize --- # --- Finalize ---
checks = ok_count + errors + warnings checks = ok_count + errors + warnings
if errors == 0 and warnings == 0 and not detailed: if errors == 0 and warnings == 0 and not 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.19 — 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)]
@@ -153,6 +153,19 @@ $finalize = {
} }
} }
# --- Format version ---
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Reference tables --- # --- Reference tables ---
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' $guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
@@ -209,7 +222,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 +233,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 +300,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")
@@ -342,11 +363,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
# Version attribute # Version attribute
$version = $root.GetAttribute("version") $version = $root.GetAttribute("version")
$versionRank = Get-FormatRank $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 ($versionRank -eq 0) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). Report-Error "1. Malformed version '$version' (expected N.N)"
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20)" } elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} }
# Detect type element — exactly one child element in md namespace # Detect type element — exactly one child element in md namespace
@@ -606,11 +631,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 +644,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,13 +1526,12 @@ 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"). $fileRank = $versionRank
function Get-FormatRank([string]$v) {
if ($v -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$fileRank = Get-FormatRank $version
if ($fileRank -gt 0) { if ($fileRank -gt 0) {
foreach ($vp in ($versionedProps.Keys | Sort-Object)) { foreach ($vp in ($versionedProps.Keys | Sort-Object)) {
$nodes = $xmlDoc.SelectNodes("//md:$vp | //xr:$vp", $ns) $nodes = $xmlDoc.SelectNodes("//md:$vp | //xr:$vp", $ns)
@@ -1,4 +1,4 @@
# meta-validate v1.13 — Validate 1C metadata object structure (Python port) (+корневой <Type>: скаляр без структуры = ошибка) # meta-validate v1.19 — 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
@@ -153,6 +175,21 @@ def finalize():
print(f"Written to: {out_file}") print(f"Written to: {out_file}")
# ── Format version ───────────────────────────────────────────
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
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
# ── Reference tables ───────────────────────────────────────── # ── Reference tables ─────────────────────────────────────────
guid_pattern = re.compile(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') guid_pattern = re.compile(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
@@ -209,7 +246,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 +257,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 +326,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"],
@@ -369,11 +414,17 @@ if root_ns != expected_ns:
# Version attribute # Version attribute
version = root.get("version", "") version = root.get("version", "")
version_rank = format_rank(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_rank == 0:
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). report_error(f"1. Malformed version '{version}' (expected N.N)")
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.20)") elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
report_warn(f"1. Format version '{version}' is below the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
report_warn(f"1. Format version '{version}' is above the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
# 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 +645,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,16 +1451,13 @@ 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", # использование общей формы в режиме совместимости интерфейса
} }
file_rank = version_rank
def format_rank(v):
""""2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', v or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
file_rank = format_rank(version)
if file_rank > 0: if file_rank > 0:
for vp in sorted(versioned_props): for vp in sorted(versioned_props):
nodes = find_all(root, f"//md:{vp} | //xr:{vp}") nodes = find_all(root, f"//md:{vp} | //xr:{vp}")
+35 -10
View File
@@ -43,24 +43,49 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J
## JSON-схема DSL ## JSON-схема DSL
Ниже — компактная структура и ключевые правила, достаточные для типового макета. Полные таблицы полей (все свойства шрифтов, стилей, ячеек), развёрнутый пример и ограничения формата — в **`reference/dsl-spec.md`**; нужны не всегда, читать по необходимости. Ниже — компактная структура и ключевые правила, достаточные для типового макета. Подробности читать по необходимости:
| Что нужно | Файл |
|---|---|
| Полные таблицы полей, развёрнутый пример, ограничения формата | `reference/dsl-spec.md` |
| Шрифты, стили, цвета, рамки, колоночные раскладки и стили колонок | `reference/styles.md` |
| Полный перечень свойств стиля — все 44 | `reference/format-properties.md` |
Краткая структура: Краткая структура:
``` ```
{ columns, page, defaultWidth, columnWidths, { columns, page, defaultWidth, columnWidths, columnStyles,
fonts: { name: { face, size, bold, italic, underline, strikeout } }, fonts: { name: { face, size, bold, italic, underline, strikeout } | { ref } },
styles: { name: { font, align, valign, border, borderWidth, wrap, format } }, styles: { name: { font, horizontalAlignment, verticalAlignment, textPlacement,
areas: [{ name, rows: [{ height, rowStyle, cells: [ backColor, textColor, border, borderColor, format, hidden } },
areas: [{ name, columnSet, rows: [{ height, hidden, rowStyle, cells: [
{ col, span, rowspan, style, param, detail, text, template } { col, span, rowspan, style, param, detail, text, template }
]}]}] ]}]}],
namedAreas: [{ name, rows, cols }],
columnSets: { name: { columns, columnWidths, columnStyles } }
} }
``` ```
Ключевые правила: Ключевые правила:
- `page` — формат страницы (`"A4-landscape"`, `"A4-portrait"` или число). Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` - `page` — формат страницы (`"A4-landscape"`, `"A4-portrait"` или число). Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"`
- `col` — 1-based позиция колонки - `name` у области в `areas` необязателен: область без имени — просто кусок сетки, именованной она не станет
- `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине) - `namedAreas` — области, которые не описываются диапазоном подряд идущих строк: полоса колонок, прямоугольник, ячейка. Тип не указывается, он следует из того, какие оси заданы
- Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template - `columnSet` у области — ссылка на раскладку из `columnSets`, когда группе строк нужны свои ширины колонок; без него действует документная раскладка
- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки) - Ключ стиля — имя свойства как в выгрузке; `columnStyles` вешает стиль на колонку так же, как `style` на ячейку
- Рамка — `border` (все стороны) или `leftBorder`/`topBorder`/`rightBorder`/`bottomBorder`; значение `"Solid"` либо `{ style, width }`
- `rowStyle` — стиль строки: ложится и на строку, и на все её колонки, заполняя пустоты (рамки по всей ширине)
- `height` и `hidden` — собственные свойства строки, у ячейки таких нет
- `empty` в строке — шорткат для N подряд пустых строк (`{ "empty": 3 }` = три `{}`) - `empty` в строке — шорткат для N подряд пустых строк (`{ "empty": 3 }` = три `{}`)
- Строку можно писать массивом ячеек — позиция из порядка, `col` не нужен: `"текст"`, `"{Имя}"` — параметр, `">"` — продолжить ячейку слева, `"|"` — сверху, `null` — пропуск колонки
- `col` — 1-based позиция колонки
- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки)
- Содержимое ячейки задаётся одним из ключей: `param` — параметр заполнения, `text` — статический текст, `template` — текст со вставками `[Параметр]`
Двухуровневая шапка массивами:
```json
"rows": [
["Вид", "Остаток", ">", "Итог"],
["|", "начало", "конец", "|"],
["{Вид}", "{Нач}", "{Кон}", "{Итог}"]
]
```
+131 -50
View File
@@ -2,6 +2,9 @@
Компактный JSON-формат для описания макетов табличных документов 1С (SpreadsheetDocument). Используется навыком `/mxl-compile` (JSON → XML). Компактный JSON-формат для описания макетов табличных документов 1С (SpreadsheetDocument). Используется навыком `/mxl-compile` (JSON → XML).
Оформление — шрифты, стили, цвета, рамки, колоночные раскладки — в `styles.md`;
полный перечень свойств стиля — в `format-properties.md`.
## Пример ## Пример
```json ```json
@@ -18,11 +21,11 @@
"styles": { "styles": {
"default": {}, "default": {},
"header": { "font": "header", "align": "center" }, "header": { "font": "header", "horizontalAlignment": "Center" },
"label": { "font": "bold" }, "label": { "font": "bold" },
"bordered": { "border": "all" }, "bordered": { "border": "Solid" },
"bordered-right": { "border": "all", "align": "right" }, "bordered-right": { "border": "Solid", "horizontalAlignment": "Right" },
"total-right": { "font": "bold", "border": "top", "align": "right" } "total-right": { "font": "bold", "topBorder": "Solid", "horizontalAlignment": "Right" }
}, },
"areas": [ "areas": [
@@ -73,88 +76,166 @@
| Поле | Обяз. | По умолч. | Описание | | Поле | Обяз. | По умолч. | Описание |
|------|:-----:|-----------|----------| |------|:-----:|-----------|----------|
| `columns` | да | — | Количество колонок | | `columns` | да | — | Количество колонок в раскладке по умолчанию. `0` допустимо: значит, все строки живут в раскладках из `columnSets` |
| `page` | нет | — | Формат страницы: `"A4-landscape"` (780), `"A4-portrait"` (540) или число. Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` | | `page` | нет | — | Формат страницы: `"A4-landscape"` (780), `"A4-portrait"` (540) или число. Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` |
| `defaultWidth` | нет | 10 | Ширина колонок по умолчанию. Игнорируется если задан `page` и все колонки используют `"Nx"` | | `defaultWidth` | нет | 10 | Ширина колонок по умолчанию. Игнорируется если задан `page` и все колонки используют `"Nx"` |
| `columnWidths` | нет | `{}` | Ширины колонок. Ключи 1-based: `"1"`, `"3-14"`, `"5,7,9"`. Значения: число (абсолют) или `"Nx"` (множитель от defaultWidth, напр. `"2x"`, `"0.5x"`) | | `columnWidths` | нет | `{}` | Ширины колонок. Ключи 1-based: `"1"`, `"3-14"`, `"5,7,9"`. Значения: число (абсолют) или `"Nx"` (множитель от defaultWidth, напр. `"2x"`, `"0.5x"`) |
| `columnStyles` | нет | — | Оформление колонок: те же ключи, значение — имя стиля (см. `styles.md`) |
| `textLanguages` | нет | `["ru"]` | Языки, на которых пишется текст, заданный строкой (см. ниже) |
| `fonts` | нет | — | Именованные шрифты (если не задано, создаётся Arial 10) | | `fonts` | нет | — | Именованные шрифты (если не задано, создаётся Arial 10) |
| `styles` | нет | `{}` | Именованные стили | | `styles` | нет | `{}` | Именованные стили (см. `styles.md`) |
| `areas` | да | — | Массив именованных областей (порядок = порядок в документе) | | `areas` | да | — | Массив областей — диапазонов подряд идущих строк (порядок = порядок в документе); имя необязательно |
| `namedAreas` | нет | — | Именованные области, заданные координатами (см. ниже) |
## Шрифты (`fonts.<name>`) | `columnSets` | нет | — | Дополнительные колоночные раскладки (см. `styles.md`) |
| Поле | По умолч. | Описание |
|------|-----------|----------|
| `face` | `"Arial"` | Имя шрифта |
| `size` | `10` | Размер |
| `bold` | `false` | Жирный |
| `italic` | `false` | Курсив |
| `underline` | `false` | Подчёркнутый |
| `strikeout` | `false` | Зачёркнутый |
Шрифт `"default"` используется когда стиль не указывает шрифт явно. Если не определён, создаётся автоматически (Arial 10).
## Стили (`styles.<name>`)
| Поле | По умолч. | Описание |
|------|-----------|----------|
| `font` | `"default"` | Ссылка на имя шрифта |
| `align` | — | `left`, `center`, `right` |
| `valign` | — | `top`, `center` |
| `border` | — | Стороны рамки: `all`, `top`, `bottom`, `left`, `right`, `none`. Через запятую: `"top,bottom"` |
| `borderWidth` | `"thin"` | Толщина рамки: `thin` (1px) или `thick` (2px) |
| `wrap` | `false` | Перенос текста |
| `format` | — | Формат данных 1С: `"ЧЦ=15; ЧДЦ=2"`, `"ДФ=dd.MM.yyyy"` и т.д. |
## Области (`areas[]`) ## Области (`areas[]`)
| Поле | Обяз. | Описание | | Поле | Обяз. | Описание |
|------|:-----:|----------| |------|:-----:|----------|
| `name` | да | Имя области для `Макет.ПолучитьОбласть("Имя")` | | `name` | нет | Имя области для `Макет.ПолучитьОбласть("Имя")` |
| `columnSet` | нет | Ссылка на раскладку из `columnSets` |
| `rows` | да | Массив строк | | `rows` | да | Массив строк |
Макет собирается из областей — диапазонов подряд идущих строк. Имя делает область именованной: она доступна в коде как `Макет.ПолучитьОбласть("Имя")` и занимает строки своего диапазона. **Область без имени** — просто кусок сетки: так описываются строки, не принадлежащие ни одной именованной области.
## Именованные области координатами (`namedAreas[]`)
Для областей, которые диапазоном подряд идущих строк не описываются: полоса колонок, прямоугольник, ячейка, а также пересекающиеся с другими.
| Поле | Обяз. | Описание |
|------|:-----:|----------|
| `name` | да | Имя области |
| `rows` | \* | Строки: число или диапазон `"N-M"`, 1-based |
| `cols` | \* | Колонки: число или диапазон `"N-M"`, 1-based |
\* Обязательна хотя бы одна из осей.
**Тип области не указывается** — он следует из того, какие оси заданы, как в `ТабличныйДокумент.Область()`: только строки → полоса строк, только колонки → полоса колонок, обе оси → прямоугольник, одиночные значения по обеим осям → одна ячейка.
```json
"namedAreas": [
{ "name": "ОбластьПечатиПоВысоте", "rows": "1-48" },
{ "name": "ОбластьПечатиПоШирине", "cols": "1-35" },
{ "name": "HZY", "rows": 9, "cols": "16-17" }
]
```
Диапазон — та же грамматика, что у `columnWidths`, но **только** число или `"N-M"`: список через запятую запрещён, область непрерывна. Имя обязательно, и хотя бы одна ось должна быть задана; нарушение любого из этих правил → ненулевой код выхода и сообщение в stderr.
## Строки (`rows[]`) ## Строки (`rows[]`)
| Поле | По умолч. | Описание | | Поле | По умолч. | Описание |
|------|-----------|----------| |------|-----------|----------|
| `height` | — | Высота строки (если не задана, используется авто) | | `height` | — | Высота строки (если не задана, используется авто) |
| `rowStyle` | — | Стиль для ВСЕХ колонок (заполняет пустоты рамками) | | `hidden` | `false` | Скрыть строку |
| `rowStyle` | — | Стиль строки: ложится и на саму строку, и на ВСЕ её колонки (заполняет пустоты рамками) |
| `cells` | `[]` | Массив ячеек | | `cells` | `[]` | Массив ячеек |
| `empty` | — | Количество подряд идущих пустых строк (заменяет N отдельных `{}`) | | `empty` | — | Количество подряд идущих пустых строк (заменяет N отдельных `{}`) |
Строка без `cells` и `rowStyle` → пустая строка. `{ "empty": 3 }` эквивалентно трём `{}`. Строка без `cells` и `rowStyle` → пустая строка. `{ "empty": 3 }` эквивалентно трём `{}`.
`height` и `hidden` — собственные свойства строки: у ячейки таких нет, и в её оформление они
не попадают. Всё остальное оформление строки задаётся через `rowStyle`.
### Короткая форма: строка массивом
Вместо объекта строка может быть массивом ячеек — позиция определяется порядком, `col` не указывается.
| Элемент | Значение |
|---------|----------|
| `"текст"` | Статический текст (`text`) |
| `{ "ru": "…", "en": "…" }` | Тот же текст на нескольких языках |
| `"{Имя}"` | Параметр (`param`) |
| `">"` | Продолжение ячейки слева — увеличивает её `span` |
| `"|"` | Продолжение ячейки сверху — увеличивает её `rowspan` |
| `null` | Пустая колонка: позиция занята, ячейка не создаётся |
| `{ ... }` | Обычная ячейка **без** `col`; нужна для `style`, `detail`, `template` |
Объект-элемент трактуется по его ключам: если среди них есть ключ ячейки (`span`, `rowspan`,
`style`, `param`, `detail`, `text`, `template`) — объект описывает свойства ячейки. Иначе он
целиком считается её текстом, а его ключи — идентификаторами языков.
```json
"rows": [
["Вид", "Остаток", ">", "Итог"],
["|", "начало", "конец", "|"],
["{Вид}", "{Нач}", "{Кон}", "{Итог}"]
]
```
Здесь «Вид» и «Итог» объединены по вертикали на две строки, «Остаток» — по горизонтали на две колонки.
Ограничения короткой формы:
- не задать `height` и `rowStyle` — это свойства строки, а не ячейки;
- не выразить текст, совпадающий с `">"`, `"|"` или с шаблоном `"{...}"`.
Маркеру нужно, что продолжать: `">"` требует ячейку слева в той же строке, `"|"` — ячейку сверху. Объектный элемент не должен нести `col`: позиция уже задана порядком. Число элементов не может превышать `columns`. Нарушение любого из этих правил → ненулевой код выхода и сообщение в stderr.
## Ячейки (`cells[]`) ## Ячейки (`cells[]`)
| Поле | Обяз. | По умолч. | Описание | | Поле | Обяз. | По умолч. | Описание |
|------|:-----:|-----------|----------| |------|:-----:|-----------|----------|
| `col` | да | — | Позиция колонки (1-based) | | `col` | да | — | Позиция колонки (1-based). В короткой форме строки не указывается — позиция берётся из порядка |
| `span` | нет | `1` | Объединение по горизонтали (количество колонок) | | `span` | нет | `1` | Объединение по горизонтали (количество колонок) |
| `rowspan` | нет | `1` | Объединение по вертикали (количество строк) | | `rowspan` | нет | `1` | Объединение по вертикали (количество строк) |
| `style` | нет | rowStyle | Стиль ячейки (переопределяет rowStyle) | | `style` | нет | rowStyle | Стиль ячейки (переопределяет rowStyle) |
| `param` | нет | — | Параметр заполнения | | `param` | нет | — | Параметр заполнения |
| `detail` | нет | — | Параметр расшифровки (только с `param`) | | `detail` | нет | — | Параметр расшифровки (только с `param`) |
| `text` | нет | — | Статический текст | | `text` | нет | — | Статический текст. Строка или объект `{ ru, en }` — см. ниже |
| `template` | нет | — | Шаблонный текст с `[Параметр]` | | `template` | нет | — | Шаблонный текст с `[Параметр]`. Строка или объект, как `text` |
### Тип заполнения ### Содержимое ячейки
Определяется автоматически по содержимому ячейки: Задаётся ровно одним из ключей, объявлять способ заполнения отдельно не нужно:
- `param` → fillType=Parameter - `param` — параметр заполнения;
- `template` → fillType=Template - `template` — текст со вставками `[Параметр]`;
- `text` → fillType=Text - `text` — статический текст;
- ничего → без fillType (пустая ячейка или рамка) - ничего — пустая ячейка (нужна, например, ради рамки).
## `rowStyle` — автозаполнение ### Текст на нескольких языках
Когда задан `rowStyle`, компилятор создаёт ячейки для ВСЕХ колонок строки. Позиции без явных ячеек заполняются пустыми ячейками с указанным стилем. Это обеспечивает сплошные рамки в табличных строках. `text` и `template` принимают строку или объект «язык → текст». Объект даёт по надписи на каждый язык, в порядке ключей. Строка означает один и тот же текст на всех языках макета — по умолчанию только русский.
```json
{ "col": 1, "text": "Наименование" }
{ "col": 2, "text": { "ru": "Поставщик", "en": "Supplier" } }
```
Набор языков задаётся документным ключом `textLanguages`:
```json
{ "columns": 3, "textLanguages": ["ru", "en"], "areas": [] }
```
С таким объявлением `"Наименование"` из примера выше даст надпись и под `ru`, и под `en`.
Ключ ни на что в конфигурации не смотрит — это просто список языков, на которые разворачивается строка.
Пустая строка — это текст: ячейка с `"text": ""` даёт пустую надпись, а не ячейку без текста.
## `rowStyle` — оформление строки
Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком.
Стиль конкретной ячейки (`style`) перекрывает `rowStyle` для этой ячейки.
Если в предыдущих строках той же области есть ячейки с `rowspan`, их колонки при автозаполнении пропускаются. Если в предыдущих строках той же области есть ячейки с `rowspan`, их колонки при автозаполнении пропускаются.
## Ограничения ## Ограничения
Текущая версия не поддерживает: DSL описывает не все конструкции табличного документа. Перечисленное ниже **теряется при
- Множественные наборы колонок (`columnsID`) round-trip** (`/mxl-decompile``/mxl-compile`): в JSON оно не попадает, в сгенерированный
- Области типа Columns / Rectangle XML не возвращается.
- Рисунки (штрихкоды, картинки)
- Фон ячеек - ячейки-поля ввода (`containsValue` / `valueType` / `controlType`);
- объединения, не привязанные к ячейке (по всей высоте или ширине документа);
- рисунки и картинки, в том числе штрихкоды, и примечания к ячейкам;
- группировки строк и колонок;
- колонтитулы, параметры печати, область печати.
Пересборка макета из DSL — это полная перегенерация, а не точечная правка XML, поэтому
diff после round-trip обычно шире фактической доработки.
Отдельно про побайтовое совпадение. В макетах, которые долго правили в Конфигураторе,
встречаются следы прежних состояний: формат ячейки может нести ширину колонки, которая с тех
пор изменилась. Такие значения не описывают итоговый документ и из него не выводятся, поэтому
собранный XML совпадёт с исходным не всегда — при полностью сохранённом содержании.
@@ -0,0 +1,77 @@
# Полный список свойств стиля
Имя ключа совпадает с именем свойства в выгрузке — исключений нет. Частые свойства
с примерами — в `styles.md`, здесь полный перечень.
Тип значения:
- **число** — целое;
- **да/нет**`true` / `false`;
- **перечисление** — одно из указанных, регистр не важен;
- **цвет**`#RRGGBB`, `style:Имя`, `web:Имя`, `win:Имя`;
- **линия**`"Solid"` либо `{ style, width }`;
- **текст** — строка (разворачивается на языки макета).
## Текст и выравнивание
| Ключ | Тип | Значение |
|------|-----|----------|
| `font` | имя | Ссылка на имя из `fonts` |
| `horizontalAlignment` | перечисление | `Left`, `Center`, `Right`, `Justify`, `Auto` |
| `verticalAlignment` | перечисление | `Top`, `Center`, `Bottom` |
| `textPlacement` | перечисление | `Wrap`, `Cut`, `Block`, `Auto` |
| `textOrientation` | число | Поворот в десятых долях градуса: `900` = 90° |
| `textColor` | цвет | Цвет текста |
| `indent` | число | Отступ текста |
| `autoIndent` | число | Автоматический отступ |
| `format` | текст | Формат данных: `"ЧЦ=15; ЧДЦ=2"` |
| `editFormat` | текст | Формат редактирования |
| `mask` | текст | Маска ввода |
| `markNegatives` | да/нет | Выделять отрицательные |
## Фон и рамка
| Ключ | Тип | Значение |
|------|-----|----------|
| `backColor` | цвет | Цвет фона |
| `pattern` | перечисление | `Solid`, `WithoutPattern`, `Pattern7`, `Pattern10`, `Pattern12`, `Pattern13`, `Pattern14`, `Pattern16` |
| `patternColor` | цвет | Цвет узора |
| `border` | линия | Все четыре стороны |
| `leftBorder`, `topBorder`, `rightBorder`, `bottomBorder` | линия | Отдельная сторона |
| `borderColor` | цвет | Цвет рамки |
## Поведение
| Ключ | Тип | Значение |
|------|-----|----------|
| `hidden` | да/нет | Скрыть |
| `protection` | да/нет | Защита от редактирования |
| `print` | да/нет | Выводить на печать |
| `hyperLink` | да/нет | Гиперссылка |
| `detailsUse` | перечисление | Использование расшифровки: `Cell`, `Row`, `WithoutProcessing` |
| `autoMarkIncomplete` | да/нет | Автоотметка незаполненного |
| `bySelectedColumns` | да/нет | По выделенным колонкам |
| `columnSizeChange` | перечисление | `Normal`, `QuickChange` |
| `autoWidthCalculation` | да/нет | Автоматический расчёт ширины |
| `widthWeightFactor` | число | Весовой коэффициент ширины |
## Картинка в ячейке
| Ключ | Тип | Значение |
|------|-----|----------|
| `picIndex` | число | Номер картинки |
| `pictureSizeMode` | перечисление | `AutoSize`, `Proportionally`, `RealSize` |
| `picHorizontalAlignment` | перечисление | `Auto`, `Center`, `Left`, `Right` |
| `picVerticalAlignment` | перечисление | `Top`, `Center`, `Bottom` |
| `textPosition` | перечисление | Положение текста относительно картинки: `Auto`, `Top`, `Right`, `Bottom` |
| `drawingBorder` | число | Рамка рисунка |
| `drawingHaveLeftBorder`, `drawingHaveTopBorder`, `drawingHaveRightBorder`, `drawingHaveBottomBorder` | да/нет | Наличие стороны рамки рисунка |
## Чего в стиле нет
- `width` — свойство колонки, задаётся через `columnWidths`;
- `height` — свойство строки, задаётся ключом `height` у строки;
- `fillType` — выводится из того, каким ключом задано содержимое ячейки
(`text` / `param` / `template`);
- `containsValue`, `valueType`, `controlType` — свойства конкретной ячейки, а не общего
оформления; сейчас не поддерживаются.
@@ -0,0 +1,145 @@
# Оформление: шрифты, стили, колонки
Оформление в табличном документе — одна сущность на всех: ячейка, строка и колонка ссылаются
на один и тот же именованный стиль. Ячейка — ключом `style`, строка — `rowStyle`, колонка —
через `columnStyles`.
## Шрифты (`fonts.<name>`)
| Поле | По умолч. | Описание |
|------|-----------|----------|
| `face` | `"Arial"` | Имя шрифта |
| `size` | `10` | Размер (бывает дробным: `8.3`) |
| `bold` | `false` | Жирный |
| `italic` | `false` | Курсив |
| `underline` | `false` | Подчёркнутый |
| `strikeout` | `false` | Зачёркнутый |
Шрифт `"default"` используется, когда стиль не указывает шрифт явно. Если не определён,
создаётся автоматически (Arial 10).
Вместо собственного описания шрифт может быть **ссылкой** — на элемент стиля конфигурации
или на системный шрифт. Тогда у него единственное поле:
```json
"fonts": {
"основной": { "ref": "style:TextFont" },
"системный": { "ref": "sys:DefaultGUIFont" }
}
```
Это та же запись, что у шрифта в описании формы.
## Стили (`styles.<name>`)
Ключ стиля — имя свойства так, как оно называется в выгрузке. Ниже частые; полный список
из 44 свойств — в `format-properties.md`.
| Поле | Описание |
|------|----------|
| `font` | Ссылка на имя из `fonts` |
| `horizontalAlignment` | `Left`, `Center`, `Right`, `Justify`, `Auto` |
| `verticalAlignment` | `Top`, `Center`, `Bottom` |
| `textPlacement` | Что делать с длинным текстом: `Wrap` (перенос), `Cut` (обрезать), `Block`, `Auto` |
| `backColor` | Цвет фона (см. «Цвет») |
| `textColor` | Цвет текста |
| `border`, `leftBorder`, `topBorder`, `rightBorder`, `bottomBorder` | Рамка (см. «Рамка») |
| `borderColor` | Цвет рамки |
| `format` | Формат данных 1С: `"ЧЦ=15; ЧДЦ=2"`, `"ДФ=dd.MM.yyyy"` |
| `hidden` | Скрыть |
| `protection` | Защита от редактирования |
| `indent` | Отступ |
| `textOrientation` | Поворот текста, в десятых долях градуса (`900` = 90°) |
Значения перечислений регистр не различают: `"center"` и `"Center"` равнозначны.
```json
"styles": {
"шапка": {
"font": "жирный",
"horizontalAlignment": "Center",
"verticalAlignment": "Center",
"textPlacement": "Wrap",
"backColor": "#EBEBEB"
},
"итог": { "font": "жирный", "topBorder": "Solid", "horizontalAlignment": "Right" }
}
```
## Цвет
Строка в одной из четырёх форм — это нотация самой платформы:
| Форма | Значение |
|-------|----------|
| `#RRGGBB` | RGB-hex, напр. `#FFFFC0` |
| `style:ИмяСтиля` | Элемент стиля конфигурации или платформы, напр. `style:FormBackColor` |
| `web:Имя` | Цвет из web-палитры, напр. `web:Gainsboro`, `web:FireBrick` |
| `win:Имя` | Системный цвет Windows, напр. `win:ButtonText` |
Имя должно существовать в своей палитре — несуществующее платформа отвергнет при загрузке.
## Рамка
Пять ключей: `border` — все четыре стороны сразу, `leftBorder` / `topBorder` / `rightBorder` /
`bottomBorder` — по отдельности. Значение одинаковое у всех:
| Запись | Значение |
|--------|----------|
| `"Solid"` | Стиль линии, ширина 1 |
| `{ "style": "Solid", "width": 2 }` | Стиль и ширина |
Стили линии: `Solid`, `None`, `Dotted`, `ThinDashed`, `LargeDashed`, `ThickDashed`, `Double`.
Задавать стороны по отдельности можно всегда: если все четыре совпали, компилятор сам свернёт
их в один `border` — так это хранит платформа.
```json
"рамка-снизу": { "bottomBorder": "Dotted" },
"рамка-вокруг": { "border": { "style": "Solid", "width": 2 } }
```
## Колоночные раскладки (`columnSets`)
Группа строк может иметь собственные ширины колонок — в 1С это «индивидуальная ширина колонок».
Документные `columns` и `columnWidths` описывают раскладку по умолчанию; дополнительные
объявляются в `columnSets`, а область ссылается на нужную ключом `columnSet` — так же, как
ячейка ссылается на `styles` через `style`.
```json
{
"columns": 52,
"columnWidths": { "1": 8 },
"columnSets": {
"таблица": { "columns": 52, "columnWidths": { "1": 7, "2-52": 24 } }
},
"areas": [
{ "name": "Шапка", "rows": [ ] },
{ "name": "ТабличнаяЧасть", "columnSet": "таблица", "rows": [ ] }
]
}
```
Раскладка описывается той же парой полей, что и документная: `columns` — количество колонок
(у раскладок оно обычно разное), `columnWidths` — ширины. Ключ словаря — имя раскладки;
в макетах, полученных декомпиляцией, это идентификатор из исходного файла, при описании
с нуля — любая строка.
Все строки области получают раскладку области, поэтому одна область не может смешивать
раскладки. Позиции колонок (`col`, `span`) проверяются по ширине раскладки СВОЕЙ области,
а не документной.
Ссылка на необъявленную раскладку → ненулевой код выхода и сообщение в stderr.
## Стиль колонки (`columnStyles`)
Колонка несёт то же оформление, что ячейка и строка. Ключи — та же грамматика диапазонов,
что у `columnWidths`; значение — имя стиля.
```json
"columnWidths": { "1": 30, "2-3": 15 },
"columnStyles": { "1": "по-центру", "4": "скрытая" }
```
Внутри `columnSets` работает тот же ключ. Ширина и стиль независимы: колонка может иметь
только ширину, только стиль или и то, и другое.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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