@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 +8 -0
- package/README.md +2 -2
- package/dist/az-editor.js +76 -41
- package/dist/library/AzHtmlEditor.d.ts +1 -0
- package/dist/library/hujjat/paragraph-format.d.ts +5 -2
- package/docs/integration.ru.md +51 -14
- package/docs/layout-metadata-v1.ru.md +9 -2
- package/package.json +1 -1
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.
|
|
17
|
+
Для воспроизводимой установки версии beta.8:
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
npm install --save-exact @makhmudov.kay/az-editor@0.1.0-beta.
|
|
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
|
-
|
|
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 =
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
let
|
|
956
|
-
|
|
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(
|
|
970
|
+
before: U(N),
|
|
960
971
|
after: null
|
|
961
972
|
}), k = !0;
|
|
962
|
-
let
|
|
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:
|
|
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:
|
|
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:
|
|
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 +=
|
|
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 = "",
|
|
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(
|
|
1386
|
-
let
|
|
1387
|
-
if (
|
|
1388
|
-
let e = parseFloat(
|
|
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
|
-
}),
|
|
1395
|
-
} else X(
|
|
1396
|
-
|
|
1397
|
-
|
|
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 && (
|
|
1439
|
+
u !== void 0 && (m += t === "firstLine" && u < 0 ? ` w:hanging="${-u}"` : ` w:${t}="${u}"`);
|
|
1405
1440
|
}
|
|
1406
|
-
|
|
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") && (
|
|
1424
|
-
return a && (a.pending ?
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 ||
|
|
2115
|
-
let a = Ue(t.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:
|
|
2118
|
-
variables:
|
|
2152
|
+
html: n.html,
|
|
2153
|
+
variables: n.variables,
|
|
2119
2154
|
warnings: a,
|
|
2120
|
-
layout:
|
|
2121
|
-
page:
|
|
2155
|
+
layout: n.layout,
|
|
2156
|
+
page: n.page,
|
|
2122
2157
|
changed: !0
|
|
2123
2158
|
};
|
|
2124
2159
|
} finally {
|
|
2125
|
-
L(
|
|
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
|
-
/**
|
|
10
|
-
|
|
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;
|
package/docs/integration.ru.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# Интеграция Az Editor в React-приложение
|
|
2
2
|
|
|
3
|
-
Руководство описывает API `@makhmudov.kay/az-editor` версии `0.1.0-beta.
|
|
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.
|
|
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
|
|
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` сохраняется.
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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()`
|
|
1050
|
-
|
|
1051
|
-
|
|
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.
|
|
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
|
|
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`. Дополнительные поля:
|