@makhmudov.kay/az-editor 0.1.0-beta.2 → 0.1.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ # Геометрия страницы и сохранение оформления
2
+
3
+ В HTML-режиме библиотека создаёт документ из ограниченного HTML. В прямом DOCX-режиме
4
+ она использует разделы исходного файла. Геометрия HTML не применяется к прямому DOCX.
5
+
6
+ ## Существующий сервер без метаданных
7
+
8
+ Используйте `<AzHtmlEditor initialHtml={html} />` без `layout`. По умолчанию: A4,
9
+ поля сверху/справа/снизу/слева 15/20/35/20 мм, колонтитулы и gutter — 0.
10
+ В OOXML это 11906 × 16838 twips, поля 850/1134/1984/1134, ширина текста 9638.
11
+ Один twip равен 1/20 pt; один дюйм — 1440 twips.
12
+
13
+ `createHujjatTemplatePayload` и `requestHujjatPreview` отправляют прежний JSON.
14
+ Новый `layout` не добавляется ими в серверные запросы. Само обновление npm-пакета
15
+ не меняет настройки mPDF и не восстанавливает данные, удалённые серверным импортом.
16
+
17
+ ## Собственная геометрия
18
+
19
+ ```ts
20
+ import { resolveHtmlLayout, type AzHtmlLayoutOptions } from '@makhmudov.kay/az-editor';
21
+
22
+ export const options: AzHtmlLayoutOptions = {
23
+ page: {
24
+ widthTwips: 11910, heightTwips: 16840, orientation: 'portrait',
25
+ margins: { top: 780, right: 1133, bottom: 280, left: 1133, header: 0, footer: 0, gutter: 0 },
26
+ },
27
+ lineSpacingPolicy: 'preserve',
28
+ };
29
+ export const metadata = resolveHtmlLayout(options);
30
+ ```
31
+
32
+ Размеры должны быть конечными положительными целыми числами до 31680 twips (22 дюйма).
33
+ Поля — неотрицательные целые; остаётся положительная область текста с учётом gutter.
34
+ Ориентация должна соответствовать размерам. Некорректный профиль отклоняется.
35
+ `resolveHtmlLayout` возвращает независимую копию с заполненными значениями по умолчанию.
36
+
37
+ `layout`, как и `initialHtml`, фиксируется при монтировании `AzHtmlEditor`.
38
+ Для другой версии, языка или профиля разрешите вопрос несохранённых правок и задайте новый React `key`.
39
+ Изменение props или вложенного объекта полей не переоткрывает черновик.
40
+
41
+ ## Что возвращает экспорт
42
+
43
+ `importHujjatTemplate` и HTML-экспорт возвращают необязательный объект:
44
+
45
+ ```ts
46
+ import type { AzLayoutMetadataV1 } from '@makhmudov.kay/az-editor';
47
+
48
+ export function isLayoutV1(value: unknown): value is AzLayoutMetadataV1 {
49
+ // Проверка только маршрутизации версии; числа дополнительно проверяет resolveHtmlLayout.
50
+ return typeof value === 'object' && value !== null &&
51
+ 'schema' in value && value.schema === 'az-editor.layout' &&
52
+ 'version' in value && value.version === 1;
53
+ }
54
+ ```
55
+
56
+ Полная форма: `{ schema: 'az-editor.layout', version: 1, page, lineSpacingPolicy }`.
57
+ Этот объект находится рядом с `html`, `warnings` и `locales`, вне языкового JSON.
58
+ После правок `page` читается из фактически сохранённого DOCX, включая изменения через
59
+ «Параметры страницы». Входной `options.page` не переписывает документ при экспорте.
60
+
61
+ Без правок `exportHtml()` возвращает исходную строку HTML, `changed: false` и известные
62
+ метаданные импорта, без сохранения и повторного разбора DOCX. `changed` описывает правки
63
+ черновика; приложение отдельно решает, нужно ли впервые сохранить профиль.
64
+
65
+ Одна версия метаданных описывает одну геометрию. Разные разделы дают `section-normalized`
66
+ с уровнем `loss`; при разной геометрии `layout` отсутствует. Исходные разделы прямого DOCX
67
+ остаются неизменными. Не заменяйте их метаданными одной страницы.
68
+
69
+ При нестандартной геометрии HTML-экспорт выдаёт `layout-not-carried-by-html`.
70
+ Если сохранить только HTML, геометрия не восстановится при открытии и не попадёт в PDF.
71
+ Пример отдельного хранения: [LayoutDraftExample.tsx](../examples/integration/LayoutDraftExample.tsx).
72
+
73
+ ## Политика интервалов
74
+
75
+ | Политика | Поведение |
76
+ | --- | --- |
77
+ | `normalize` (по умолчанию) | Множитель меньше 1 увеличивается до 1. При DOCX → HTML тесный абсолютный интервал становится `normal` с предупреждением. Проверяются размеры всех видимых фрагментов и численное вертикальное смещение. |
78
+ | `preserve` | Сохраняет представимые численные значения, включая множители 124/240 и 158/240. Возвращает предупреждение о риске наложения. |
79
+
80
+ Проверка размера фрагментов — оценка, а не измерение ascent/descent всех шрифтов.
81
+ Большие буквы, формулы, разные начертания и смещения требуют проверки на реальных данных.
82
+ Абсолютный HTML `line-height` импортируется как Word `atLeast`. По CSS невозможно узнать,
83
+ был ли в исходнике `exact`; при DOCX → HTML это различие отмечено `line-rule-lost`.
84
+ Численное `w:position` не сохраняется в действующем HTML-контракте: `baseline-shift-lost`
85
+ имеет уровень `loss`. `w:spacing` сохраняется как `letter-spacing`.
86
+
87
+ ## Диагностика и шрифты
88
+
89
+ `HujjatWarning` сохраняет `code`, `message`, `severity`. Дополнительные поля:
90
+ `stage` (`import`, `export`, `edit`, `fonts`), `location`, `before`, `after`.
91
+ `location` может содержать `part`, `paragraphIndex`, `runIndex`, `sectionIndex`, `htmlPath`.
92
+ Индексы начинаются с нуля, относятся к одному снимку и не являются постоянными ID.
93
+
94
+ `onDiagnostics` у обоих компонентов сообщает риски исходного/сохранённого DOCX, не меняя
95
+ его. После правки предыдущая диагностика очищается; новый анализ выполняется при
96
+ явном `save()`. Для самостоятельного анализа есть `analyzeDocxFormatting(bytes)`.
97
+ HTML-экспорт возвращает собственные предупреждения преобразования в результате вызова.
98
+ Полный текст документа в отчёт исходной диагностики не включается.
99
+
100
+ `status.fonts` содержит `state` и `warnings`. `loading` означает ожидание источников;
101
+ `ready` — источники предоставлены без известных ошибок; `degraded` — известная
102
+ подстановка или ошибка; `unknown` — недостаточно сведений. При загрузке источников
103
+ HTML-черновик остаётся в фазе `loading`. Ошибка шрифта не уничтожает черновик: он может
104
+ перейти в `ready` с `fonts.state: 'degraded'` и предупреждением.
105
+
106
+ Состояние `ready` не удостоверяет совпадение с Word. Проверяются предоставленные
107
+ источники, таблицы подстановок с weight/style и сообщение движка о недоступных семействах.
108
+ Таблица конфигурации может содержать подстановки для ещё не используемых начертаний.
109
+ Встроенные шрифты и отсутствующие глифы не позволяют считать пустой список доказательством
110
+ точного совпадения. `document.fonts.ready` сам по себе недостаточен для ресурсов движка.
111
+ Передавайте стабильный `fonts`; для замены набора шрифтов разрешите правки и перемонтируйте редактор.
112
+
113
+ ## Управление абзацами
114
+
115
+ Оба ref предоставляют `setParagraphPagination({ keepNext?, keepLines?, widowControl?, pageBreakBefore? })`.
116
+ Значения boolean применяются к текущему выделению одной командой с undo. `false` явно
117
+ выключает наследованное правило; пропущенное поле не изменяется. Во время загрузки,
118
+ экспорта HTML, `actionsDisabled` или режима `view` команда отклоняется.
119
+
120
+ Для кнопки используйте `onMouseDown={event => event.preventDefault()}`, чтобы сохранить
121
+ выделение. HTML поддерживает `page-break-before`, `page-break-after`, `page-break-inside`;
122
+ `widowControl` не входит в контракт и при экспорте даёт `widow-control-lost` уровня `loss`.
123
+ Табуляция/позиции/заполнители также дают адресованные `loss`. Приложение должно показать
124
+ их пользователю перед вызовом с `allowLossy: true`; автоматического выравнивания пробелами
125
+ или превращения в таблицы нет.
126
+
127
+ ## Согласование с сервером
128
+
129
+ Для собственного профиля отдельно согласуйте версию метаданных, хранение рядом с языком
130
+ и версией шаблона, валидацию, применение при preview и финальном PDF, поля колонтитулов,
131
+ шрифты всех начертаний и миграцию старых записей. Имя HTTP-поля здесь не задаётся.
132
+
133
+ Приёмка: один исходник → серверный HTML → редактор → сохранённый HTML → серверный PDF.
134
+ Проверяйте текст, заголовки и подписи, переносы и страницы на одинаковых шрифтах и полях.
135
+ Без фактического серверного HTML и эталона Word PDF совпадение этой цепочки не проверено.
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  AzDocumentEditor, DOCX_MIME, downloadDocx, readDocxFile,
4
- type AzDocumentEditorHandle, type EditorStatus,
4
+ type AzDocumentEditorHandle, type EditorStatus, type HujjatWarning,
5
5
  } from '@makhmudov.kay/az-editor';
6
6
  import '@makhmudov.kay/az-editor/styles.css';
7
7
 
@@ -18,6 +18,7 @@ export function DocxEditorExample() {
18
18
  const [dirty, setDirty] = useState(false);
19
19
  const [busy, setBusy] = useState(false);
20
20
  const [error, setError] = useState('');
21
+ const [diagnostics,setDiagnostics] = useState<HujjatWarning[]>([]);
21
22
 
22
23
  useEffect(() => {
23
24
  if (!dirty) return;
@@ -82,11 +83,15 @@ export function DocxEditorExample() {
82
83
  <span role="status">{busy ? 'Обработка…' : dirty ? 'Есть несохранённые правки' : 'Нет несохранённых правок'} · Страниц: {status.pages}</span>
83
84
  </div>
84
85
  {(error || status.error) && <p role="alert">{error || status.error}</p>}
86
+ {[...diagnostics,...(status.fonts?.warnings ?? [])].length > 0 && <details>
87
+ <summary>Проверка оформления</summary>
88
+ <ul>{[...diagnostics,...(status.fonts?.warnings ?? [])].map((w,i)=><li key={i}>{w.message}{w.location?.paragraphIndex !== undefined ? ` (абзац ${w.location.paragraphIndex+1})`:''}</li>)}</ul>
89
+ </details>}
85
90
  <AzDocumentEditor key={document.key} ref={editor} document={document.bytes}
86
91
  title={document.name} style={{ height: 720 }} mode={busy ? 'view' : 'edit'}
87
92
  actionsDisabled={busy || status.loading || !!status.error}
88
93
  onOpen={() => input.current?.click()} onSave={() => void save()}
89
94
  onChange={() => { revision.current += 1; setDirty(true); }}
90
- onStatusChange={setStatus} onFontError={setError} />
95
+ onStatusChange={setStatus} onFontError={setError} onDiagnostics={setDiagnostics} />
91
96
  </section>;
92
97
  }
@@ -45,6 +45,7 @@ export function HtmlDraft({ initialHtml, variables = [], onApply, onCancel }: Ht
45
45
  <button type="button" disabled={status.phase === 'exporting'} onClick={cancel}>Закрыть черновик</button>
46
46
  </div>
47
47
  {error && <p role="alert">{error}</p>}
48
+ {!!status.fonts?.warnings.length && <details><summary>Шрифты и переносы</summary><ul>{status.fonts.warnings.map((w,i)=><li key={i}>{w.message}</li>)}</ul></details>}
48
49
  {!!status.warnings.length && <details open={status.phase === 'blocked' || !!pending}>
49
50
  <summary>Изменения при преобразовании ({status.warnings.length})</summary>
50
51
  <ul>{status.warnings.map((w, i) => <li key={i}>{w.message}</li>)}</ul>
@@ -0,0 +1,39 @@
1
+ import {useRef,useState} from 'react';
2
+ import {AzHtmlEditor,resolveHtmlLayout,type AzHtmlEditorHandle,type AzHtmlEditorStatus,type AzLayoutMetadataV1,type AzHtmlExportResult} from '@makhmudov.kay/az-editor';
3
+ import '@makhmudov.kay/az-editor/styles.css';
4
+
5
+ export interface StoredDraft { id:string; version:number; locale:string; html:string; layout:AzLayoutMetadataV1 }
6
+ /** The host implements storage after agreeing its metadata contract with the server. */
7
+ export function LayoutDraftExample({draft,onApply}:{draft:StoredDraft;onApply:(result:{html:string;layout:AzLayoutMetadataV1})=>Promise<void>}){
8
+ return <Session key={`${draft.id}:${draft.version}:${draft.locale}`} draft={draft} onApply={onApply}/>;
9
+ }
10
+ function Session({draft,onApply}:{draft:StoredDraft;onApply:(result:{html:string;layout:AzLayoutMetadataV1})=>Promise<void>}){
11
+ const ref=useRef<AzHtmlEditorHandle>(null),busy=useRef(false);
12
+ const [status,setStatus]=useState<AzHtmlEditorStatus>(),[pending,setPending]=useState<AzHtmlExportResult>(),[error,setError]=useState(''),[saving,setSaving]=useState(false);
13
+ async function apply(result:AzHtmlExportResult){
14
+ if(busy.current)return;
15
+ if(!result.layout){setError('Разные разделы не описываются одной геометрией. Требуется отдельное решение.');return;}
16
+ busy.current=true;setSaving(true);
17
+ try{await onApply({html:result.html,layout:resolveHtmlLayout(result.layout)});setPending(undefined);}
18
+ catch(e){setError(String(e));}finally{busy.current=false;setSaving(false);}
19
+ }
20
+ async function prepare(){
21
+ if(busy.current||status?.phase!=='ready')return;
22
+ try{const result=await ref.current!.exportHtml({allowLossy:true});if(result.warnings.length)setPending(result);else await apply(result);}
23
+ catch(e){setError(String(e));}
24
+ }
25
+ return <section>
26
+ <button disabled={saving||!!pending||status?.phase!=='ready'} onClick={()=>void prepare()}>Применить HTML и профиль</button>
27
+ <button disabled={saving||!!pending||status?.phase!=='ready'} onMouseDown={e=>e.preventDefault()} onClick={()=>{try{ref.current!.setParagraphPagination({keepNext:true,keepLines:true});}catch(e){setError(String(e));}}}>
28
+ Связать абзац со следующим
29
+ </button>
30
+ {pending&&<div role="alert"><ul>{pending.warnings.map((w,i)=><li key={i}>{w.message}</li>)}</ul>
31
+ <button disabled={saving} onClick={()=>void apply(pending)}>Принять указанные изменения</button>
32
+ <button disabled={saving} onClick={()=>setPending(undefined)}>Вернуться к правкам</button>
33
+ </div>}
34
+ {error&&<p role="alert">{error}</p>}
35
+ {status?.phase==='blocked'&&<button onClick={()=>void ref.current!.acceptImportLoss().catch(e=>setError(String(e)))}>Открыть с потерями: {status.warnings.map(w=>w.message).join(' ')}</button>}
36
+ <AzHtmlEditor ref={ref} initialHtml={draft.html} layout={draft.layout} onStatusChange={setStatus}
37
+ mode={pending||saving ? 'view':'edit'} actionsDisabled={!!pending||saving} style={{height:700}}/>
38
+ </section>;
39
+ }
@@ -10,6 +10,7 @@
10
10
  | `HtmlDraft.tsx` | Применение/отмена, подтверждение потерь, переменные |
11
11
  | `server-api.ts` | Пример HTTP-контрактов сохранения и PDF |
12
12
  | `loadFonts.ts` | Загрузка файлов шрифтов в конфигурацию `fonts` |
13
+ | `LayoutDraftExample.tsx` | Черновик с отдельными метаданными страницы после согласования серверного контракта |
13
14
 
14
15
  Для `HtmlFormExample` скопируйте и `HtmlDraft`. Остальные примеры независимы.
15
16
  Файлы входят в npm-архив как исходники, не как отдельные экспорты пакета.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makhmudov.kay/az-editor",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.4",
4
4
  "type": "module",
5
5
  "types": "./dist/library/index.d.ts",
6
6
  "sideEffects": [
@@ -17,6 +17,8 @@
17
17
  "files": [
18
18
  "dist",
19
19
  "docs/integration.ru.md",
20
+ "docs/layout-metadata-v1.ru.md",
21
+ "CHANGELOG.md",
20
22
  "examples/integration",
21
23
  "THIRD_PARTY_NOTICES.md",
22
24
  "LICENSE"
@@ -48,6 +50,7 @@
48
50
  },
49
51
  "devDependencies": {
50
52
  "@playwright/test": "1.64.0",
53
+ "@types/jsdom": "^30.0.0",
51
54
  "@types/node": "26.6.4",
52
55
  "@types/react": "19.3.0",
53
56
  "@types/react-dom": "19.3.0",