@makhmudov.kay/az-editor 0.1.0-beta.7 → 0.1.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Изменения
2
2
 
3
+ ## 0.1.0-beta.8
4
+
5
+ - HTML → DOCX сохраняет шрифт знака абзаца и наследуемый шрифт `<br>`. Обратный экспорт сохраняет семейство и размер знака абзаца отдельно от текста. Авторские пустые строки не удаляются.
6
+ - Нормализация абсолютных интервалов учитывает эффективные размеры Word в половинах пункта, знак абзаца и противоположные смещения фрагментов. Тесный интервал нормализуется при импорте; `line-height-metrics-estimated` явно обозначает отсутствие реальных метрик шрифта в конвертере.
7
+ - `AzHtmlEditor.exportHtml()` возвращает эффективный нормализованный HTML с `changed: true` даже без ручной правки. `dirty` по-прежнему обозначает ручные изменения. Интеграции должны вызывать экспорт перед сохранением независимо от `dirty`.
8
+ - Потеря скрытого знака абзаца при HTML-экспорте диагностируется как `hidden-paragraph-mark-lost` уровня `loss`; его скрытый шрифт не активируется молча.
9
+ - `preserve`, авторские разрывы и сетка таблиц сохраняют прежний контракт. Недостающий во входном HTML шрифт пустых строк библиотека не восстанавливает по первому текстовому фрагменту. Различие Word `exact`/`atLeast` всё ещё требует расширения сохраняемого контракта.
10
+
3
11
  ## 0.1.0-beta.7
4
12
 
5
13
  - Сетка таблицы рассчитывается по ограничениям всех строк одновременно. Пересекающиеся `colspan` больше не зависят от порядка обработки; сохраняются совместимые суммы в неполной сетке, `rowspan` и независимые вложенные таблицы.
package/README.md CHANGED
@@ -14,10 +14,10 @@ React-библиотека для редактирования DOCX и HTML-до
14
14
  npm install @makhmudov.kay/az-editor@beta
15
15
  ```
16
16
 
17
- Для воспроизводимой установки версии beta.7:
17
+ Для воспроизводимой установки версии beta.8:
18
18
 
19
19
  ```sh
20
- npm install --save-exact @makhmudov.kay/az-editor@0.1.0-beta.7
20
+ npm install --save-exact @makhmudov.kay/az-editor@0.1.0-beta.8
21
21
  ```
22
22
 
23
23
  Требуются React и React DOM 19, Node.js `^24.15.0 || >=26.0.0` для установки/сборки,
package/dist/az-editor.js CHANGED
@@ -719,7 +719,13 @@ function ve(e) {
719
719
  return e.rule === "auto" ? String(e.value / 240) : `${e.value / 20}pt`;
720
720
  }
721
721
  function ye(e) {
722
- return (parseFloat(e["font-size"]) || 12) + Math.abs(Number(e["--az-position"]) || 0) / 2;
722
+ let t = 0, n = 0;
723
+ for (let r of e) {
724
+ if (r["--az-hidden"] === "1") continue;
725
+ let e = parseFloat(r["font-size"]) || 12, i = (Number(r["--az-position"]) || 0) / 2;
726
+ t = Math.max(t, e + i), n = Math.max(n, -i);
727
+ }
728
+ return t + n;
723
729
  }
724
730
  //#endregion
725
731
  //#region src/library/hujjat/docx-chrome.ts
@@ -947,25 +953,30 @@ async function Te(e, t = {}, n) {
947
953
  parseFloat(s["text-indent"] ?? "0") < 0 && parseFloat(s["margin-left"] ?? "0") >= 0 && (s["padding-left"] = s["margin-left"] ?? "0pt", s["margin-left"] = "0pt");
948
954
  let _ = _e([B(B(b, "pPrDefault"), "pPr"), ...o]);
949
955
  _ && (s["line-height"] = ve(_));
950
- let C = Object.assign({}, S, ...i.map((e) => K(B(e, "rPr")))), O = Array.from(e.getElementsByTagNameNS(R, "r")).map((e) => {
951
- let t = B(e, "rPr"), n = Object.assign({}, C, ...w(H(t, "rStyle")).map((e) => K(B(e, "rPr"))), K(t));
952
- return n["--az-hidden"] === "1" ? 0 : ye(n);
953
- }), A = s["line-height"];
954
- A && (/^\d*\.?\d+$/.test(A) ? Number(A) < 1 : A.endsWith("pt") && parseFloat(A) < Math.max(ye(C), ...O)) && (t.lineSpacingPolicy === "preserve" ? m("line-height-overlap-risk", "Авторский тесный интервал сохранён; строки могут перекрываться. Фактические метрики шрифта требуют проверки.", "warning", A, A) : (s["line-height"] = /^\d*\.?\d+$/.test(A) ? "1" : "normal", m("line-height-normalized", "Тесный межстрочный интервал нормализован по размерам фрагментов. Проверьте переносы на используемых шрифтах.", "warning", A, s["line-height"]))), _ && _.rule !== "auto" && m("line-rule-lost", "CSS line-height не сохраняет различие правил Word exact и atLeast.", "warning", _.rule, "CSS line-height"), o.some((e) => B(e, "tabs")) && h("tab-stops-removed", "Позиции, типы и заполнители табуляции не поддерживаются HTML-блоком.");
955
- let j = [B(B(b, "pPrDefault"), "pPr"), ...o].map((e) => B(e, "widowControl")).filter(Boolean).at(-1);
956
- j && M(u, "widow-control-lost", "Правило висячих строк Word не представлено в HTML-контракте.", "loss", {
956
+ let C = Object.assign({}, S, ...i.map((e) => K(B(e, "rPr")))), O = Object.assign({}, C, K(B(r, "rPr")));
957
+ O["--az-hidden"] === "1" && (m("hidden-paragraph-mark-lost", "Скрытый знак абзаца нельзя представить в HTML; его отдельный шрифт заменён стилем абзаца.", "loss", "hidden", "visible paragraph style"), O = {
958
+ ...C,
959
+ "--az-hidden": "0"
960
+ }), s["font-family"] = O["font-family"], s["font-size"] = O["font-size"];
961
+ let A = Array.from(e.getElementsByTagNameNS(R, "r")).map((e) => {
962
+ let t = B(e, "rPr");
963
+ return Object.assign({}, C, ...w(H(t, "rStyle")).map((e) => K(B(e, "rPr"))), K(t));
964
+ }), j = s["line-height"];
965
+ j && (/^\d*\.?\d+$/.test(j) ? Number(j) < 1 : j.endsWith("pt") && parseFloat(j) < ye([O, ...A])) && (t.lineSpacingPolicy === "preserve" ? m("line-height-overlap-risk", "Авторский тесный интервал сохранён; строки могут перекрываться. Фактические метрики шрифта требуют проверки.", "warning", j, j) : (s["line-height"] = /^\d*\.?\d+$/.test(j) ? "1" : "normal", m("line-height-normalized", "Тесный межстрочный интервал нормализован по размерам фрагментов. Проверьте переносы на используемых шрифтах.", "warning", j, s["line-height"]))), j?.endsWith("pt") && t.lineSpacingPolicy !== "preserve" && m("line-height-metrics-estimated", "Проверка интервала использует размеры и смещения фрагментов, а не метрики установленного шрифта. Результат нормализации требует проверки на используемых шрифтах.", "warning", j, s["line-height"]), _ && _.rule !== "auto" && m("line-rule-lost", "CSS line-height не сохраняет различие правил Word exact и atLeast.", "warning", _.rule, "CSS line-height"), o.some((e) => B(e, "tabs")) && h("tab-stops-removed", "Позиции, типы и заполнители табуляции не поддерживаются HTML-блоком.");
966
+ let N = [B(B(b, "pPrDefault"), "pPr"), ...o].map((e) => B(e, "widowControl")).filter(Boolean).at(-1);
967
+ N && M(u, "widow-control-lost", "Правило висячих строк Word не представлено в HTML-контракте.", "loss", {
957
968
  stage: "export",
958
969
  location: { ...f },
959
- before: U(j),
970
+ before: U(N),
960
971
  after: null
961
972
  }), k = !0;
962
- let N = Array.from(e.children).filter((e) => e !== r).map((e) => P(e, C, n + 1)).join("").split("<div style=\"page-break-before:always\"></div>").map((e, t) => `${t ? "<div style=\"page-break-before:always\"></div>" : ""}<p${de(s)}>${e || "<br>"}</p>`).join("");
973
+ let F = Array.from(e.children).filter((e) => e !== r).map((e) => P(e, C, n + 1)).join("").split("<div style=\"page-break-before:always\"></div>").map((e, t) => `${t ? "<div style=\"page-break-before:always\"></div>" : ""}<p${de(s)}>${e || "<br>"}</p>`).join("");
963
974
  if (l && l !== "0") {
964
975
  let e = E.get(l), t = z(D.get(H(e, "abstractNumId")), "lvl").find((e) => V(e, "ilvl") === String(d));
965
- if (!t) return M(u, "numbering-unsupported", "Не найдена нумерация списка.", "loss"), { html: N };
976
+ if (!t) return M(u, "numbering-unsupported", "Не найдена нумерация списка.", "loss"), { html: F };
966
977
  let n = H(t, "numFmt");
967
978
  return (!["decimal", "bullet"].includes(n) || d || H(t, "start") && H(t, "start") !== "1" || B(e, "lvlOverride")) && M(u, "numbering-normalized", "Сложная нумерация/начальное число списка заменены обычным HTML-списком.", "loss"), {
968
- html: N,
979
+ html: F,
969
980
  list: {
970
981
  id: l,
971
982
  level: Math.min(d, 8),
@@ -974,7 +985,7 @@ async function Te(e, t = {}, n) {
974
985
  };
975
986
  }
976
987
  return {
977
- html: N,
988
+ html: F,
978
989
  continuation: H(r, "pStyle") === "AzHujjatListContinuation"
979
990
  };
980
991
  }
@@ -1372,38 +1383,62 @@ async function Fe(e, t, n = E(), r) {
1372
1383
  }
1373
1384
  function _(e, t) {
1374
1385
  let n = "";
1375
- for (let r of e) r.nodeType === 3 ? r.textContent && (n += `<w:r><w:rPr>${g(t)}</w:rPr><w:t xml:space="preserve">${ue(r.textContent)}</w:t></w:r>`) : r instanceof Element && (r.localName === "br" ? n += "<w:r><w:br/></w:r>" : n += _(r.childNodes, Q(r, t)));
1386
+ for (let r of e) r.nodeType === 3 ? r.textContent && (n += `<w:r><w:rPr>${g(t)}</w:rPr><w:t xml:space="preserve">${ue(r.textContent)}</w:t></w:r>`) : r instanceof Element && (r.localName === "br" ? n += `<w:r><w:rPr>${g(Q(r, t))}</w:rPr><w:br/></w:r>` : n += _(r.childNodes, Q(r, t)));
1376
1387
  return n;
1377
1388
  }
1378
1389
  function v(e, r, a, o = i) {
1379
- let s = "", c = r["text-align"];
1390
+ let s = Array.from(e), c = "", l = r["text-align"];
1380
1391
  [
1381
1392
  "left",
1382
1393
  "right",
1383
1394
  "center",
1384
1395
  "justify"
1385
- ].includes(c) && (s += `<w:jc w:val="${c === "justify" ? "both" : c}"/>`);
1386
- let l = Math.max(o.before ?? 0, Z(r["margin-top"]) ?? 0), u = Math.max(o.after ?? 0, Z(r["margin-bottom"]) ?? (o.cell ? 0 : 120)), d = "", f = r["line-height"];
1387
- if (f && /^\d*\.?\d+%?$/.test(f)) {
1388
- let e = parseFloat(f) / (f.endsWith("%") ? 100 : 1), r = n.lineSpacingPolicy === "preserve";
1396
+ ].includes(l) && (c += `<w:jc w:val="${l === "justify" ? "both" : l}"/>`);
1397
+ let u = Math.max(o.before ?? 0, Z(r["margin-top"]) ?? 0), d = Math.max(o.after ?? 0, Z(r["margin-bottom"]) ?? (o.cell ? 0 : 120)), f = "", p = r["line-height"];
1398
+ if (p && /^\d*\.?\d+%?$/.test(p)) {
1399
+ let e = parseFloat(p) / (p.endsWith("%") ? 100 : 1), r = n.lineSpacingPolicy === "preserve";
1389
1400
  e < 1 && M(t, r ? "line-height-overlap-risk" : "line-height-normalized", r ? "Тесный интервал сохранён; проверьте наложение строк." : "Тесный интервал увеличен до одинарного; проверьте переносы.", "warning", {
1390
1401
  stage: "import",
1391
1402
  location: { htmlPath: o.htmlPath },
1392
1403
  before: e,
1393
1404
  after: r ? e : 1
1394
- }), d = ` w:line="${Math.round(Math.max(+!r, e) * 240)}" w:lineRule="auto"`;
1395
- } else X(f) !== void 0 && (d = ` w:line="${Math.max(0, Z(f))}" w:lineRule="atLeast"`);
1396
- s += `<w:spacing w:before="${Math.max(0, l)}" w:after="${Math.max(0, u)}"${d}/>`;
1397
- let p = "";
1405
+ }), f = ` w:line="${Math.round(Math.max(+!r, e) * 240)}" w:lineRule="auto"`;
1406
+ } else if (X(p) !== void 0) {
1407
+ let e = [], i = (t) => e.push({
1408
+ ...t,
1409
+ "font-size": `${Math.round((X(t["font-size"]) ?? 12) * 2) / 2}pt`
1410
+ }), a = (e, t) => {
1411
+ for (let n of e) if (n.nodeType === 3 && n.textContent) i(t);
1412
+ else if (n instanceof Element) {
1413
+ let e = Q(n, t);
1414
+ n.localName === "br" ? i(e) : a(n.childNodes, e);
1415
+ }
1416
+ };
1417
+ i(r), a(s, r);
1418
+ let c = Z(p) / 20 < ye(e), l = n.lineSpacingPolicy === "normalize";
1419
+ c && M(t, l ? "line-height-normalized" : "line-height-overlap-risk", l ? "Тесный абсолютный интервал заменён естественным одинарным интервалом." : "Тесный абсолютный интервал сохранён; проверьте наложение строк.", "warning", {
1420
+ stage: "import",
1421
+ location: { htmlPath: o.htmlPath },
1422
+ before: p,
1423
+ after: l ? "normal" : p
1424
+ }), l && M(t, "line-height-metrics-estimated", "Проверка интервала использует размеры фрагментов, а не метрики установленного шрифта. Результат требует проверки на используемых шрифтах.", "warning", {
1425
+ stage: "import",
1426
+ location: { htmlPath: o.htmlPath },
1427
+ before: p,
1428
+ after: c ? "normal" : p
1429
+ }), f = c && l ? " w:line=\"240\" w:lineRule=\"auto\"" : ` w:line="${Math.max(0, Z(p))}" w:lineRule="atLeast"`;
1430
+ }
1431
+ c += `<w:spacing w:before="${Math.max(0, u)}" w:after="${Math.max(0, d)}"${f}/>`;
1432
+ let m = "";
1398
1433
  for (let [e, t] of [
1399
1434
  ["margin-left", "left"],
1400
1435
  ["margin-right", "right"],
1401
1436
  ["text-indent", "firstLine"]
1402
1437
  ]) {
1403
1438
  let n = t === "left" ? o.left : t === "right" ? o.right : void 0, i = t === "left" || t === "right" ? Math.max(0, Z(r[`padding-${t}`]) ?? 0) : 0, s = Z(r[e]), c = s !== void 0 || i ? (s ?? 0) + i : void 0, l = a ? t === "left" ? 360 : t === "firstLine" ? a.pending ? -180 : 0 : void 0 : void 0, u = c === void 0 && !n && l === void 0 ? void 0 : (c ?? (t === "firstLine" ? l : 0) ?? 0) + (n ?? 0) + (t === "left" ? l ?? 0 : 0);
1404
- u !== void 0 && (p += t === "firstLine" && u < 0 ? ` w:hanging="${-u}"` : ` w:${t}="${u}"`);
1439
+ u !== void 0 && (m += t === "firstLine" && u < 0 ? ` w:hanging="${-u}"` : ` w:${t}="${u}"`);
1405
1440
  }
1406
- p && (s += `<w:ind${p}/>`);
1441
+ m && (c += `<w:ind${m}/>`);
1407
1442
  for (let [e, t, n] of [
1408
1443
  [
1409
1444
  "page-break-before",
@@ -1420,8 +1455,8 @@ async function Fe(e, t, n = E(), r) {
1420
1455
  "keepNext",
1421
1456
  "avoid"
1422
1457
  ]
1423
- ]) (r[e] === n || r[e] === "auto") && (s += `<w:${t} w:val="${r[e] === n ? "1" : "0"}"/>`);
1424
- return a && (a.pending ? s += `<w:numPr><w:ilvl w:val="0"/><w:numId w:val="${a.id}"/></w:numPr>` : s += "<w:pStyle w:val=\"AzHujjatListContinuation\"/>", a.pending = !1), `<w:p><w:pPr>${s}</w:pPr>${_(e, r)}</w:p>`;
1458
+ ]) (r[e] === n || r[e] === "auto") && (c += `<w:${t} w:val="${r[e] === n ? "1" : "0"}"/>`);
1459
+ return a && (a.pending ? c += `<w:numPr><w:ilvl w:val="0"/><w:numId w:val="${a.id}"/></w:numPr>` : c += "<w:pStyle w:val=\"AzHujjatListContinuation\"/>", a.pending = !1), `<w:p><w:pPr>${c}<w:rPr>${g(r)}</w:rPr></w:pPr>${_(s, r)}</w:p>`;
1425
1460
  }
1426
1461
  function y(e, t) {
1427
1462
  let n = e.style, r = n.getPropertyValue(`border-${t}-style`) || n.borderStyle, i = n.getPropertyValue(`border-${t}-width`) || n.borderWidth, a = n.getPropertyValue(`border-${t}-color`) || n.borderColor;
@@ -1714,10 +1749,10 @@ async function Re(e) {
1714
1749
  s,
1715
1750
  ...i.map((e) => B(e, "pPr")),
1716
1751
  r
1717
- ]), l = Object.assign({}, c, ...i.map((e) => K(B(e, "rPr")))), f = ye(l);
1752
+ ]), l = Object.assign({}, c, ...i.map((e) => K(B(e, "rPr")))), f = [Object.assign({}, l, K(B(r, "rPr")))];
1718
1753
  Array.from(e.getElementsByTagNameNS(R, "r")).forEach((e, t) => {
1719
1754
  let r = B(e, "rPr"), i = Object.assign({}, l, ...d(H(r, "rStyle")).map((e) => K(B(e, "rPr"))), K(r));
1720
- i["--az-hidden"] !== "1" && (f = Math.max(f, ye(i)), Number(i["--az-position"]) && M(a, "source-baseline-shift", "Исходный фрагмент смещён по вертикали; сохранён без нормализации.", "warning", {
1755
+ i["--az-hidden"] !== "1" && (f.push(i), Number(i["--az-position"]) && M(a, "source-baseline-shift", "Исходный фрагмент смещён по вертикали; сохранён без нормализации.", "warning", {
1721
1756
  stage: "import",
1722
1757
  location: {
1723
1758
  ...n,
@@ -1726,7 +1761,7 @@ async function Re(e) {
1726
1761
  before: Number(i["--az-position"]) / 2,
1727
1762
  after: Number(i["--az-position"]) / 2
1728
1763
  }));
1729
- }), o && (o.rule === "auto" ? o.value < 240 : o.value / 20 < f) && M(a, "source-tight-spacing", "Авторский тесный интервал сохранён. Проверьте наложение строк на используемых шрифтах.", "warning", {
1764
+ }), o && (o.rule === "auto" ? o.value < 240 : o.value / 20 < ye(f)) && M(a, "source-tight-spacing", "Авторский тесный интервал сохранён. Проверьте наложение строк на используемых шрифтах.", "warning", {
1730
1765
  stage: "import",
1731
1766
  location: n,
1732
1767
  before: `${o.rule}:${o.value}`,
@@ -2090,8 +2125,8 @@ var We = e(function(e, a) {
2090
2125
  return r(a, () => ({
2091
2126
  async exportHtml(e = {}) {
2092
2127
  if (!D.current || w.current.phase !== "ready" || !E.current || !T.current) throw new A("HTML-редактор ещё не готов или занят.");
2093
- let t = E.current;
2094
- if (!w.current.dirty) return {
2128
+ let t = E.current, n = t.warnings.some((e) => e.code === "line-height-normalized");
2129
+ if (!w.current.dirty && !n) return {
2095
2130
  html: _,
2096
2131
  variables: [...t.variables],
2097
2132
  warnings: [...t.warnings],
@@ -2099,30 +2134,30 @@ var We = e(function(e, a) {
2099
2134
  page: structuredClone(v),
2100
2135
  changed: !1
2101
2136
  };
2102
- let n = O.current, r = k.current;
2137
+ let r = O.current, i = k.current;
2103
2138
  M({
2104
2139
  phase: "exporting",
2105
2140
  error: null
2106
2141
  });
2107
2142
  try {
2108
- let i = await T.current.exportHujjat({
2143
+ let n = await T.current.exportHujjat({
2109
2144
  locale: "uz",
2110
2145
  allowLossy: !0,
2111
2146
  layout: y,
2112
2147
  page: t.page
2113
2148
  });
2114
- if (!D.current || n !== O.current || r !== k.current) throw new A("Черновик изменился или закрыт во время экспорта. Повторите экспорт актуального документа.");
2115
- let a = Ue(t.warnings, i.warnings);
2149
+ if (!D.current || r !== O.current || i !== k.current) throw new A("Черновик изменился или закрыт во время экспорта. Повторите экспорт актуального документа.");
2150
+ let a = Ue(t.warnings, n.warnings);
2116
2151
  return M({ warnings: a }), N(a, e.allowLossy), {
2117
- html: i.html,
2118
- variables: i.variables,
2152
+ html: n.html,
2153
+ variables: n.variables,
2119
2154
  warnings: a,
2120
- layout: i.layout,
2121
- page: i.page,
2155
+ layout: n.layout,
2156
+ page: n.page,
2122
2157
  changed: !0
2123
2158
  };
2124
2159
  } finally {
2125
- L(n);
2160
+ L(r);
2126
2161
  }
2127
2162
  },
2128
2163
  async acceptImportLoss() {
@@ -13,6 +13,7 @@ export interface AzHtmlEditorStatus {
13
13
  fonts?: AzFontStatus;
14
14
  }
15
15
  export interface AzHtmlExportResult extends HujjatHtmlResult {
16
+ /** True when the draft was edited or import normalized its formatting. */
16
17
  changed: boolean;
17
18
  }
18
19
  export interface AzHtmlEditorHandle {
@@ -6,5 +6,8 @@ export interface LineSpacing {
6
6
  /** Resolve partial OOXML properties before reducing them to CSS. Values stay in native units. */
7
7
  export declare function resolveLineSpacing(properties: (Element | null)[]): LineSpacing | undefined;
8
8
  export declare function lineSpacingCss(spacing: LineSpacing): string;
9
- /** Conservative size check, not a measurement of the installed font's ascent/descent. */
10
- export declare function runHeightEstimate(css: Css): number;
9
+ /** Nominal em-box envelope in points, not measured font ascent/descent/leading.
10
+ * Oppositely shifted runs occupy both sides of the baseline; max(run height)
11
+ * alone underestimates their combined extent. Callers supply point font sizes.
12
+ */
13
+ export declare function paragraphHeightEstimate(fonts: Css[]): number;
@@ -1,9 +1,12 @@
1
1
  # Интеграция Az Editor в React-приложение
2
2
 
3
- Руководство описывает API `@makhmudov.kay/az-editor` версии `0.1.0-beta.7`.
3
+ Руководство описывает API `@makhmudov.kay/az-editor` версии `0.1.0-beta.8`.
4
4
  Поддержка серверного `page`, описанная ниже, добавлена в beta.5.
5
5
  Совместимые исправления разрывов и строк таблиц добавлены в beta.6 и описаны ниже.
6
6
  Исправление общей сетки таблиц и более строгая диагностика размеров добавлены в beta.7.
7
+ В beta.8 исправлено сохранение шрифта пустых строк и знака абзаца, уточнена нормализация
8
+ интервалов. `exportHtml()` сохраняет результат нормализации и без ручных правок;
9
+ вызывайте его перед сохранением независимо от `status.dirty`.
7
10
  Примеры написаны на TypeScript. Для JavaScript удалите аннотации типов и `import type`.
8
11
 
9
12
  ## Содержание
@@ -71,7 +74,7 @@ npm install @makhmudov.kay/az-editor@beta
71
74
  Для фиксации опубликованной версии:
72
75
 
73
76
  ```sh
74
- npm install --save-exact @makhmudov.kay/az-editor@0.1.0-beta.7
77
+ npm install --save-exact @makhmudov.kay/az-editor@0.1.0-beta.8
75
78
  ```
76
79
 
77
80
  Тег `beta` может указывать на более новую версию. Сохраняйте lockfile и проверяйте
@@ -271,9 +274,10 @@ type HtmlResult = {
271
274
  };
272
275
  ```
273
276
 
274
- - Без правок возвращается исходный `initialHtml`, `changed: false`. Повторной очистки строки нет.
275
- - После правок возвращается преобразованный HTML, `changed: true`.
276
- - `dirty`/`changed` отражают факт правок, а не сравнение строк; undo до исходного вида не гарантирует `dirty: false`.
277
+ - Без правок и без нормализации интервалов при импорте возвращается исходный `initialHtml`, `changed: false`. Повторной очистки строки нет.
278
+ - После правок **или нормализации интервала при импорте** возвращается преобразованный HTML, `changed: true`. Нормализация сохраняется и без ручного ввода текста.
279
+ - `dirty` отражает ручные правки; `changed` учитывает также нормализацию. Это не сравнение строк; undo до исходного вида не гарантирует `dirty: false`.
280
+ - При сохранении всегда вызывайте `exportHtml()`, даже если `dirty: false`. Потери преобразования по-прежнему требуют `allowLossy: true`; принятие потерь при открытии само по себе не разрешает их сохранить.
277
281
  - Экспорт не сбрасывает `dirty` и не подтверждает запись на сервере.
278
282
  - Повторный одновременный экспорт, экспорт до готовности, после закрытия или из изменившегося во время операции черновика отклоняется.
279
283
 
@@ -1032,12 +1036,21 @@ CSS ограничен шрифтом/цветом, базовым оформл
1032
1036
  пустую страницу. Автоматическая пагинация по-прежнему зависит от шрифтов и геометрии.
1033
1037
 
1034
1038
  Без `page.paragraph_spacing` абзацы внутри ячеек не получают дополнительный отступ 6pt после текста;
1035
- явный `margin-bottom` сохраняется. Значение `line-height` в абсолютных единицах
1036
- задаёт минимальную, а не жёсткую высоту строки (`atLeast`), чтобы крупный текст
1037
- не перекрывал соседнюю строку. Числа и проценты задают множитель обычной строки.
1038
- По умолчанию множитель меньше 1 приводится к 1. При DOCX → HTML интервал ниже размера шрифта
1039
- нормализуется, включая крупные фрагменты внутри абзаца; это отмечается
1040
- предупреждением `line-height-normalized`. Достаточные интервалы сохраняются.
1039
+ явный `margin-bottom` сохраняется. Абсолютный HTML `line-height` задаёт минимальную
1040
+ высоту строки (`atLeast`), если политика не нормализовала его. Числа и проценты
1041
+ задают множитель обычной строки. По умолчанию (`normalize`) множитель меньше 1
1042
+ приводится к 1; тесный абсолютный интервал при HTML → DOCX сразу заменяется
1043
+ естественным одинарным (`auto:240`). При DOCX → HTML он становится `normal`.
1044
+ `line-height-normalized` сообщает исходное и эффективное значения и адрес абзаца.
1045
+ После сохранения эффективного результата повторное открытие не нормализует его снова.
1046
+
1047
+ Оценка учитывает размер знака абзаца, видимые фрагменты и обе стороны смещений
1048
+ базовой линии. При явно заданном небольшом размере знака абзаца размер по умолчанию
1049
+ не должен увеличивать оценку. Это оценка по размерам, **не измерение метрик шрифтов**:
1050
+ конвертер не получает ascent/descent/leading установленных шрифтов. Для абсолютных
1051
+ интервалов в `normalize` возвращается `line-height-metrics-estimated`, даже если
1052
+ интервал оставлен без изменения. Проверяйте реальные шрифты и начертания в редакторе
1053
+ и PDF; эта диагностика не гарантирует отсутствие пересечений всех глифов.
1041
1054
 
1042
1055
  С `layout.lineSpacingPolicy: 'preserve'` численные интервалы сохраняются с предупреждением
1043
1056
  о наложении. `exact` и `atLeast` неразличимы в CSS; `line-rule-lost` отмечает эту потерю
@@ -1046,9 +1059,33 @@ CSS ограничен шрифтом/цветом, базовым оформл
1046
1059
  Табуляция, `widowControl` и численное смещение `w:position` дают `loss` при экспорте в HTML;
1047
1060
  при прямом сохранении DOCX эти свойства не нормализуются.
1048
1061
 
1049
- `AzHtmlEditor.exportHtml()` без правок возвращает исходный HTML. Нормализованный
1050
- HTML появляется после редактирования. Для окончательного PDF также нужны
1051
- совпадающие шрифты, поля страницы и правила CSS на сервере.
1062
+ `AzHtmlEditor.exportHtml()` возвращает нормализованный HTML с `changed: true`,
1063
+ даже если пользователь ещё не печатал. `dirty` в таком случае остаётся `false`.
1064
+ При низкоуровневом `importHujjatTemplate()` нормализуется `result.document`;
1065
+ поле `result.html` содержит очищенный вход, а не обратный экспорт модели.
1066
+ Для получения эффективного HTML используйте `exportHujjatDocx(result.document, …)`.
1067
+ Для окончательного PDF нужны совпадающие шрифты, поля страницы и правила CSS на сервере.
1068
+
1069
+ Шрифт абзаца и шрифт его текста — разные данные. Импорт сохраняет свойства шрифта
1070
+ абзаца на знаке абзаца, а `<br>` получает свойства своего HTML-контейнера.
1071
+ Экспорт сохраняет семейство и размер знака абзаца в стилях `<p>`, отдельно от `<span>`.
1072
+ Скрытый знак абзаца не представим в текущем HTML: `hidden-paragraph-mark-lost`
1073
+ уровня `loss` сообщает замену его отдельного шрифта обычным стилем абзаца.
1074
+ Строгий экспорт требует принять эту потерю, прямой DOCX остаётся без такой подмены.
1075
+ Например, для строк и четырёх завершающих переносов размером 10 pt передавайте:
1076
+
1077
+ ```html
1078
+ <p style="font-family:freesans;font-size:10pt;line-height:1.1583333333333334">
1079
+ Текст<br><br><br><br>
1080
+ </p>
1081
+ ```
1082
+
1083
+ Если 10 pt задан только на `<span>Текст</span>`, завершающие `<br>` вне него и знак
1084
+ абзаца наследуют размер самого `<p>` или документа (по умолчанию 12 pt).
1085
+ Библиотека не копирует стиль первого фрагмента на весь абзац: в Word эти стили могут
1086
+ различаться намеренно. Восстанавливающий оформление импортёр должен передать фактические
1087
+ свойства абзаца и переносов из источника. Служебные и авторские пустые строки нельзя
1088
+ удалять ради желаемого количества страниц.
1052
1089
 
1053
1090
  ### Лимиты и оформление
1054
1091
 
@@ -80,16 +80,23 @@ export function isLayoutV1(value: unknown): value is AzLayoutMetadataV1 {
80
80
 
81
81
  | Политика | Поведение |
82
82
  | --- | --- |
83
- | `normalize` (по умолчанию) | Множитель меньше 1 увеличивается до 1. При DOCX → HTML тесный абсолютный интервал становится `normal` с предупреждением. Проверяются размеры всех видимых фрагментов и численное вертикальное смещение. |
83
+ | `normalize` (по умолчанию) | Множитель меньше 1 увеличивается до 1. Тесный абсолютный HTML-интервал становится `auto:240` уже при импорте, при DOCX → HTML — `normal`. Проверяются размер знака абзаца, размеры видимых фрагментов и обе стороны численных смещений базовой линии. |
84
84
  | `preserve` | Сохраняет представимые численные значения, включая множители 124/240 и 158/240. Возвращает предупреждение о риске наложения. |
85
85
 
86
86
  Проверка размера фрагментов — оценка, а не измерение ascent/descent всех шрифтов.
87
+ Для абсолютного интервала в `normalize` возвращается адресное предупреждение
88
+ `line-height-metrics-estimated`, включая случаи без изменения значения.
87
89
  Большие буквы, формулы, разные начертания и смещения требуют проверки на реальных данных.
88
- Абсолютный HTML `line-height` импортируется как Word `atLeast`. По CSS невозможно узнать,
90
+ Абсолютный HTML `line-height`, оставленный без нормализации, импортируется как Word `atLeast`. По CSS невозможно узнать,
89
91
  был ли в исходнике `exact`; при DOCX → HTML это различие отмечено `line-rule-lost`.
90
92
  Численное `w:position` не сохраняется в действующем HTML-контракте: `baseline-shift-lost`
91
93
  имеет уровень `loss`. `w:spacing` сохраняется как `letter-spacing`.
92
94
 
95
+ `AzHtmlEditor.exportHtml()` сериализует нормализацию даже без ручной правки:
96
+ `changed: true`, при этом `status.dirty` остаётся `false`. Всегда вызывайте экспорт
97
+ перед сохранением. При повторном открытии сохранённого эффективного HTML нормализация
98
+ не повторяется; `preserve` по-прежнему не увеличивает интервалы молча.
99
+
93
100
  ## Диагностика и шрифты
94
101
 
95
102
  `HujjatWarning` сохраняет `code`, `message`, `severity`. Дополнительные поля:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makhmudov.kay/az-editor",
3
- "version": "0.1.0-beta.7",
3
+ "version": "0.1.0-beta.8",
4
4
  "type": "module",
5
5
  "types": "./dist/library/index.d.ts",
6
6
  "sideEffects": [