mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
e5685e5c7b
commit
261971cc41
9 files changed
+1072
-4
No files matched your search
+21
-1
@@ -262,6 +262,26 @@
|
|||||||
<div class="form-actions"><div id="objectStyleStatus" class="form-status" role="status"></div><button id="objectStyleCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
<div class="form-actions"><div id="objectStyleStatus" class="form-status" role="status"></div><button id="objectStyleCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<dialog id="structureDialog" class="form-dialog structure-dialog" aria-label="管理标引">
|
||||||
|
<form id="structureForm">
|
||||||
|
<div class="form-row"><label for="structureEntry">标引</label><select id="structureEntry"></select></div>
|
||||||
|
<div id="structureNodeRow" class="form-row"><label for="structureNodeSelect">节点</label><select id="structureNodeSelect"></select></div>
|
||||||
|
<div id="structureActionRow" class="form-row"><label for="structureAction">操作</label><select id="structureAction"><option value="update">修改</option><option value="add">新增</option><option value="bind">绑定</option><option value="delete">删除</option></select></div>
|
||||||
|
<div id="structureFields">
|
||||||
|
<div id="structureTypeRow" class="form-row"><label for="structureType">类型</label><input id="structureType" type="text" required></div>
|
||||||
|
<div class="form-row"><label for="structureName">名称</label><input id="structureName" type="text" required></div>
|
||||||
|
<div class="form-row"><label for="structureNamespace">命名空间</label><input id="structureNamespace" type="text"></div>
|
||||||
|
<div class="form-row"><label for="structureText">内容</label><textarea id="structureText" rows="2"></textarea></div>
|
||||||
|
<div class="panel-row"><span>属性</span><button id="structureAttributeAdd" class="small-button" type="button" title="添加属性">添加</button></div>
|
||||||
|
<div id="structureAttributes" class="structure-edit-list"></div>
|
||||||
|
</div>
|
||||||
|
<div id="structureBinding" hidden>
|
||||||
|
<div class="panel-row"><span>引用</span><div class="panel-actions"><button id="structureSelection" class="small-button" type="button" title="绑定选区">选区</button><button id="structureReferenceAdd" class="small-button" type="button" title="添加引用">添加</button></div></div>
|
||||||
|
<div id="structureReferences" class="structure-edit-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions"><div id="structureStatus" class="form-status" role="status"></div><button id="structureCancel" class="button" type="button">取消</button><button id="structureSubmit" class="button" type="submit">确定</button></div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
<dialog id="outlinePanel" class="form-dialog" aria-label="编辑目录">
|
<dialog id="outlinePanel" class="form-dialog" aria-label="编辑目录">
|
||||||
<form id="outlineForm">
|
<form id="outlineForm">
|
||||||
<div class="form-row"><label for="outlineTitle">标题</label><input id="outlineTitle" type="text" required autofocus></div>
|
<div class="form-row"><label for="outlineTitle">标题</label><input id="outlineTitle" type="text" required autofocus></div>
|
||||||
@@ -705,7 +725,7 @@
|
|||||||
<div id="attachmentList" class="attachment-list"></div>
|
<div id="attachmentList" class="attachment-list"></div>
|
||||||
</section>
|
</section>
|
||||||
<section id="structurePanel" class="structure-panel" aria-labelledby="structurePanelTitle" hidden>
|
<section id="structurePanel" class="structure-panel" aria-labelledby="structurePanelTitle" hidden>
|
||||||
<div class="panel-row"><div id="structurePanelTitle" class="panel-title compact">结构</div><span id="structureSummary" class="count-pill">0</span></div>
|
<div class="panel-row"><div id="structurePanelTitle" class="panel-title compact">结构</div><div class="panel-actions"><span id="structureSummary" class="count-pill">0</span><button id="structureManage" class="small-button" type="button" title="管理标引" aria-haspopup="dialog" aria-controls="structureDialog" hidden>管理</button></div></div>
|
||||||
<div id="structureList" class="structure-list"></div>
|
<div id="structureList" class="structure-list"></div>
|
||||||
</section>
|
</section>
|
||||||
<section id="signaturePanel" class="signature-panel" aria-labelledby="signaturePanelTitle" hidden>
|
<section id="signaturePanel" class="signature-panel" aria-labelledby="signaturePanelTitle" hidden>
|
||||||
|
|||||||
@@ -3037,6 +3037,57 @@ audio.page-media {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.structure-dialog {
|
||||||
|
width: min(560px, calc(100vw - 32px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.structure-dialog .form-row {
|
||||||
|
grid-template-columns: 4em minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.structure-dialog .form-row > label {
|
||||||
|
text-align-last: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.structure-edit-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.structure-attribute {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 28px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.structure-attribute .structure-attribute-value {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.structure-attribute > button {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.structure-reference + .structure-reference {
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.structure-reference .form-unit {
|
||||||
|
min-width: 0;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.structure-reference .form-unit > input,
|
||||||
|
.structure-reference .form-unit > select,
|
||||||
|
.structure-attribute > input {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.info-line.ok .info-value {
|
.info-line.ok .info-value {
|
||||||
color: var(--accent-strong);
|
color: var(--accent-strong);
|
||||||
}
|
}
|
||||||
|
|||||||
+196
-3
@@ -540,6 +540,22 @@ const el = {
|
|||||||
structurePanel: document.querySelector("#structurePanel"),
|
structurePanel: document.querySelector("#structurePanel"),
|
||||||
structureList: document.querySelector("#structureList"),
|
structureList: document.querySelector("#structureList"),
|
||||||
structureSummary: document.querySelector("#structureSummary"),
|
structureSummary: document.querySelector("#structureSummary"),
|
||||||
|
structureManage: document.querySelector("#structureManage"),
|
||||||
|
structureDialog: document.querySelector("#structureDialog"),
|
||||||
|
structureForm: document.querySelector("#structureForm"),
|
||||||
|
structureEntry: document.querySelector("#structureEntry"),
|
||||||
|
structureNodeSelect: document.querySelector("#structureNodeSelect"),
|
||||||
|
structureAction: document.querySelector("#structureAction"),
|
||||||
|
structureFields: document.querySelector("#structureFields"),
|
||||||
|
structureType: document.querySelector("#structureType"),
|
||||||
|
structureName: document.querySelector("#structureName"),
|
||||||
|
structureNamespace: document.querySelector("#structureNamespace"),
|
||||||
|
structureText: document.querySelector("#structureText"),
|
||||||
|
structureAttributes: document.querySelector("#structureAttributes"),
|
||||||
|
structureBinding: document.querySelector("#structureBinding"),
|
||||||
|
structureReferences: document.querySelector("#structureReferences"),
|
||||||
|
structureStatus: document.querySelector("#structureStatus"),
|
||||||
|
structureSubmit: document.querySelector("#structureSubmit"),
|
||||||
signatureSummary: document.querySelector("#signatureSummary"),
|
signatureSummary: document.querySelector("#signatureSummary"),
|
||||||
signatureList: document.querySelector("#signatureList"),
|
signatureList: document.querySelector("#signatureList"),
|
||||||
annotationNote: document.querySelector("#annotationNote"),
|
annotationNote: document.querySelector("#annotationNote"),
|
||||||
@@ -1211,6 +1227,32 @@ el.objectStyleForm.addEventListener("submit", async event => {
|
|||||||
if (await changeDocument("ofdgoStyleObjects", { ...item, id: canvasEditor.items().map(member => member.id) }, style)) el.objectStylePanel.close();
|
if (await changeDocument("ofdgoStyleObjects", { ...item, id: canvasEditor.items().map(member => member.id) }, style)) el.objectStylePanel.close();
|
||||||
});
|
});
|
||||||
el.outlineCancel.addEventListener("click", () => el.outlinePanel.close());
|
el.outlineCancel.addEventListener("click", () => el.outlinePanel.close());
|
||||||
|
el.structureManage.addEventListener("click", openStructureDialog);
|
||||||
|
document.querySelector("#structureCancel").addEventListener("click", () => el.structureDialog.close());
|
||||||
|
el.structureEntry.addEventListener("change", refreshStructureNodes);
|
||||||
|
el.structureNodeSelect.addEventListener("change", refreshStructureFields);
|
||||||
|
el.structureAction.addEventListener("change", refreshStructureFields);
|
||||||
|
document.querySelector("#structureAttributeAdd").addEventListener("click", () => addStructureAttribute());
|
||||||
|
document.querySelector("#structureReferenceAdd").addEventListener("click", () => addStructureReference());
|
||||||
|
document.querySelector("#structureSelection").addEventListener("click", () => {
|
||||||
|
const items = canvasEditor.items();
|
||||||
|
if (!items.length) { el.structureStatus.textContent = "尚未选择图元"; return; }
|
||||||
|
const page = state.doc.pages[items[0].index];
|
||||||
|
addStructureReference({ pageId: page.id, objectIds: items.map(item => item.id) });
|
||||||
|
});
|
||||||
|
el.structureForm.addEventListener("submit", async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!state.editing || document.body.hasAttribute("aria-busy")) return;
|
||||||
|
const index = Number(el.structureEntry.value), path = JSON.parse(el.structureNodeSelect.value || "[]");
|
||||||
|
const action = index < 0 ? "create" : el.structureAction.value;
|
||||||
|
const node = { name: el.structureName.value, namespace: el.structureNamespace.value, text: el.structureText.value,
|
||||||
|
attributes: [...el.structureAttributes.children].map(row => ({ name: row.querySelector('[name="name"]').value, namespace: row.querySelector('[name="namespace"]').value, value: row.querySelector('[name="value"]').value })) };
|
||||||
|
const references = [...el.structureReferences.children].map(row => ({ pageId: row.querySelector('[name="page"]').value,
|
||||||
|
objectIds: row.querySelector('[name="ids"]').value.split(/[,\s]+/u).filter(Boolean) })).filter(ref => ref.objectIds.length);
|
||||||
|
el.structureStatus.textContent = "";
|
||||||
|
const saved = await changeDocument("ofdgoChangeStructure", null, JSON.stringify({ action, index, path, typeId: el.structureType.value, node, references }));
|
||||||
|
if (saved) el.structureDialog.close();
|
||||||
|
});
|
||||||
el.outlineForm.addEventListener("submit", async event => {
|
el.outlineForm.addEventListener("submit", async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const path = state.outlineAction === "add" ? JSON.parse(el.outlineParent.value) : state.outlineSelection;
|
const path = state.outlineAction === "add" ? JSON.parse(el.outlineParent.value) : state.outlineSelection;
|
||||||
@@ -1657,7 +1699,7 @@ function handleKeyDown(event) {
|
|||||||
|
|
||||||
function formDialogOpen() {
|
function formDialogOpen() {
|
||||||
return Boolean(sealPreview) || fontBindingElements.Panel.open || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open || el.documentPanel.open
|
return Boolean(sealPreview) || fontBindingElements.Panel.open || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open || el.documentPanel.open
|
||||||
|| el.versionPanel.open || el.batchPagesPanel.open || el.objectStylePanel.open || el.sourceTextPanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
|| el.structureDialog.open || el.versionPanel.open || el.batchPagesPanel.open || el.objectStylePanel.open || el.sourceTextPanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setObjectDimension(input, value) {
|
function setObjectDimension(input, value) {
|
||||||
@@ -3414,6 +3456,8 @@ async function changeDocument(name, item, ...args) {
|
|||||||
el.objectBoundsStatus.textContent = err.message;
|
el.objectBoundsStatus.textContent = err.message;
|
||||||
} else if (el.outlinePanel.open) {
|
} else if (el.outlinePanel.open) {
|
||||||
el.outlineStatus.textContent = err.message;
|
el.outlineStatus.textContent = err.message;
|
||||||
|
} else if (el.structureDialog.open) {
|
||||||
|
el.structureStatus.textContent = err.message;
|
||||||
} else if (el.infoPanel.open) {
|
} else if (el.infoPanel.open) {
|
||||||
el.infoStatus.textContent = err.message;
|
el.infoStatus.textContent = err.message;
|
||||||
} else if (el.attachmentDialog.open) {
|
} else if (el.attachmentDialog.open) {
|
||||||
@@ -4188,6 +4232,7 @@ async function openOFD(file, remote = null) {
|
|||||||
el.sourceTextPanel.close();
|
el.sourceTextPanel.close();
|
||||||
el.objectBoundsPanel.close();
|
el.objectBoundsPanel.close();
|
||||||
el.outlinePanel.close();
|
el.outlinePanel.close();
|
||||||
|
el.structureDialog.close();
|
||||||
updateControls();
|
updateControls();
|
||||||
await openDocument({ doc, pageIndex: remote?.pageIndex || 0, resetScroll: true, openSeq, activateActions: true });
|
await openDocument({ doc, pageIndex: remote?.pageIndex || 0, resetScroll: true, openSeq, activateActions: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -8150,7 +8195,7 @@ function renderMeta(keepDetails = false) {
|
|||||||
el.metaSignatures.textContent = doc.detailsPending ? "正在检查签名" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0);
|
el.metaSignatures.textContent = doc.detailsPending ? "正在检查签名" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0);
|
||||||
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError, state.editing], renderAttachments);
|
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError, state.editing], renderAttachments);
|
||||||
renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
|
renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
|
||||||
renderMetaContent(el.structureList, [doc.structures, doc.structureError, doc.documentIndex, doc.documentVersion, state.openSeq, state.editorInfo?.revision], renderStructures);
|
updateStructureControls();
|
||||||
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
|
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
|
||||||
refreshEditorFonts();
|
refreshEditorFonts();
|
||||||
updateLocalFontButton();
|
updateLocalFontButton();
|
||||||
@@ -8227,9 +8272,149 @@ function renderAttachments() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openStructureDialog() {
|
||||||
|
if (!state.editing || !state.doc || document.body.hasAttribute("aria-busy")) return;
|
||||||
|
el.structureEntry.replaceChildren();
|
||||||
|
for (const [index, structure] of (state.doc.structures || []).entries()) {
|
||||||
|
el.structureEntry.add(new Option(`${index + 1} · ${structure.root?.name || structure.typeId}`, index));
|
||||||
|
}
|
||||||
|
el.structureEntry.add(new Option("新建标引", -1));
|
||||||
|
el.structureAction.value = "update";
|
||||||
|
refreshStructureNodes();
|
||||||
|
el.structureDialog.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshStructureNodes() {
|
||||||
|
const structure = state.doc.structures?.[Number(el.structureEntry.value)];
|
||||||
|
el.structureNodeSelect.replaceChildren();
|
||||||
|
const append = (node, path, depth) => {
|
||||||
|
if (node.references?.length) return;
|
||||||
|
el.structureNodeSelect.add(new Option(`${" ".repeat(depth)}${node.name}`, JSON.stringify(path)));
|
||||||
|
for (const [index, child] of (node.children || []).entries()) append(child, [...path, index], depth + 1);
|
||||||
|
};
|
||||||
|
if (structure?.root) append(structure.root, [], 0);
|
||||||
|
refreshStructureFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshStructureFields() {
|
||||||
|
const index = Number(el.structureEntry.value), creating = index < 0;
|
||||||
|
const structure = state.doc.structures?.[index];
|
||||||
|
let node = structure?.root, canBind = Boolean(structure?.canBind);
|
||||||
|
for (const part of JSON.parse(el.structureNodeSelect.value || "[]")) {
|
||||||
|
node = node?.children?.[part];
|
||||||
|
canBind &&= node?.namespace === structure.root.namespace;
|
||||||
|
}
|
||||||
|
canBind &&= node?.name !== "ObjectRef";
|
||||||
|
el.structureAction.querySelector('[value="bind"]').disabled = !canBind;
|
||||||
|
if (!canBind && el.structureAction.value === "bind") el.structureAction.value = "update";
|
||||||
|
if (!creating && !node) el.structureAction.value = "delete";
|
||||||
|
for (const option of el.structureAction.options) option.disabled = option.value === "bind" ? !canBind : !creating && !node && option.value !== "delete";
|
||||||
|
const action = creating ? "create" : el.structureAction.value;
|
||||||
|
document.querySelector("#structureNodeRow").hidden = creating || !node;
|
||||||
|
document.querySelector("#structureActionRow").hidden = creating;
|
||||||
|
document.querySelector("#structureTypeRow").hidden = !creating;
|
||||||
|
el.structureType.value = creating ? "custom" : structure?.typeId || "";
|
||||||
|
el.structureFields.hidden = action === "bind" || action === "delete";
|
||||||
|
el.structureBinding.hidden = action !== "bind";
|
||||||
|
el.structureName.value = action === "update" ? node?.name || "" : "";
|
||||||
|
el.structureNamespace.value = creating ? "" : node?.namespace || "";
|
||||||
|
el.structureText.value = action === "update" ? node?.text || "" : "";
|
||||||
|
el.structureAttributes.replaceChildren();
|
||||||
|
el.structureReferences.replaceChildren();
|
||||||
|
if (action === "update") for (const attr of node?.attributes || []) addStructureAttribute(attr);
|
||||||
|
if (action === "bind") {
|
||||||
|
const references = [...(node?.references || []), ...(node?.children || []).flatMap(child => child.references || [])];
|
||||||
|
for (const ref of references) addStructureReference(ref);
|
||||||
|
if (!references.length) addStructureReference();
|
||||||
|
}
|
||||||
|
for (const control of el.structureFields.querySelectorAll("input,textarea,button")) control.disabled = el.structureFields.hidden;
|
||||||
|
el.structureType.disabled = !creating;
|
||||||
|
el.structureStatus.textContent = structure?.error || (action === "delete" ? "删除此节点及子节点" : "");
|
||||||
|
el.structureSubmit.textContent = ({ create: "新增", add: "新增", update: "修改", bind: "绑定", delete: "删除" })[action];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStructureAttribute(attribute = {}) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "structure-attribute";
|
||||||
|
for (const [name, label] of [["name", "属性名称"], ["namespace", "命名空间"], ["value", "属性内容"]]) {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.name = name;
|
||||||
|
input.value = attribute[name] || "";
|
||||||
|
input.placeholder = label;
|
||||||
|
input.setAttribute("aria-label", label);
|
||||||
|
input.required = name === "name";
|
||||||
|
if (name === "value") input.className = "structure-attribute-value";
|
||||||
|
row.append(input);
|
||||||
|
}
|
||||||
|
const remove = document.createElement("button");
|
||||||
|
remove.type = "button";
|
||||||
|
remove.className = "small-button";
|
||||||
|
remove.textContent = "×";
|
||||||
|
remove.title = "删除属性";
|
||||||
|
remove.setAttribute("aria-label", remove.title);
|
||||||
|
remove.addEventListener("click", () => row.remove());
|
||||||
|
row.append(remove);
|
||||||
|
el.structureAttributes.append(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStructureReference(reference = {}) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "structure-reference";
|
||||||
|
const page = document.createElement("select"), object = document.createElement("select"), ids = document.createElement("input");
|
||||||
|
page.name = "page";
|
||||||
|
ids.name = "ids";
|
||||||
|
ids.value = (reference.objectIds || []).join(",");
|
||||||
|
for (const [index, item] of state.doc.pages.entries()) page.add(new Option(`第${index + 1}页`, item.id));
|
||||||
|
if (reference.pageId) page.value = reference.pageId;
|
||||||
|
const remove = document.createElement("button"), add = document.createElement("button");
|
||||||
|
for (const [button, label, text] of [[remove, "删除引用", "×"], [add, "添加图元", "添加"]]) {
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "small-button";
|
||||||
|
button.textContent = text;
|
||||||
|
button.title = label;
|
||||||
|
button.setAttribute("aria-label", label);
|
||||||
|
}
|
||||||
|
remove.addEventListener("click", () => row.remove());
|
||||||
|
add.addEventListener("click", () => {
|
||||||
|
if (object.value) ids.value = [...ids.value.split(/[,\s]+/u).filter(Boolean), object.value].join(",");
|
||||||
|
});
|
||||||
|
for (const [label, control, button] of [["页码", page, remove], ["图元", object, add], ["顺序", ids, null]]) {
|
||||||
|
const line = document.createElement("div"), title = document.createElement("label"), unit = document.createElement("div");
|
||||||
|
line.className = "form-row";
|
||||||
|
title.textContent = label;
|
||||||
|
control.setAttribute("aria-label", label);
|
||||||
|
unit.className = "form-unit";
|
||||||
|
unit.append(control);
|
||||||
|
if (button) unit.append(button);
|
||||||
|
line.append(title, unit);
|
||||||
|
row.append(line);
|
||||||
|
}
|
||||||
|
el.structureReferences.append(row);
|
||||||
|
const load = async () => {
|
||||||
|
const value = page.value, sequence = state.openSeq;
|
||||||
|
object.replaceChildren(new Option("读取中", ""));
|
||||||
|
add.disabled = true;
|
||||||
|
try {
|
||||||
|
const index = state.doc.pages.findIndex(item => item.id === value);
|
||||||
|
const targets = await callWASM("ofdgoStructureObjects", index);
|
||||||
|
if (!row.isConnected || sequence !== state.openSeq || value !== page.value) return;
|
||||||
|
object.replaceChildren(new Option("选择图元", ""));
|
||||||
|
for (const target of targets || []) object.add(new Option(`${target.id} · ${(target.text || "图元").slice(0, 60)}`, target.id));
|
||||||
|
add.disabled = false;
|
||||||
|
} catch (err) {
|
||||||
|
if (!row.isConnected || sequence !== state.openSeq || value !== page.value) return;
|
||||||
|
object.replaceChildren(new Option("读取失败", ""));
|
||||||
|
el.structureStatus.textContent = err.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
page.addEventListener("change", () => { ids.value = ""; load(); });
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
function renderStructures() {
|
function renderStructures() {
|
||||||
const structures = state.doc?.structures || [];
|
const structures = state.doc?.structures || [];
|
||||||
el.structurePanel.hidden = !structures.length && !state.doc?.structureError;
|
el.structurePanel.hidden = !structures.length && !state.doc?.structureError && !(state.doc && state.editing);
|
||||||
|
el.structureManage.hidden = !state.editing;
|
||||||
el.structureSummary.textContent = structures.length;
|
el.structureSummary.textContent = structures.length;
|
||||||
el.structureList.replaceChildren();
|
el.structureList.replaceChildren();
|
||||||
if (state.doc?.structureError) appendInfoLine(el.structureList, "错误", state.doc.structureError, "fail");
|
if (state.doc?.structureError) appendInfoLine(el.structureList, "错误", state.doc.structureError, "fail");
|
||||||
@@ -8239,6 +8424,11 @@ function renderStructures() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateStructureControls() {
|
||||||
|
renderMetaContent(el.structureList, [state.doc?.structures, state.doc?.structureError, state.doc?.documentIndex, state.doc?.documentVersion, state.openSeq, state.editorInfo?.revision, state.editing], renderStructures);
|
||||||
|
el.structureManage.disabled = !state.ready || state.exporting || document.body.hasAttribute("aria-busy") || Boolean(state.doc?.detailsPending);
|
||||||
|
}
|
||||||
|
|
||||||
function structureNode(node, index, path) {
|
function structureNode(node, index, path) {
|
||||||
const details = document.createElement("details");
|
const details = document.createElement("details");
|
||||||
details.className = "structure-node";
|
details.className = "structure-node";
|
||||||
@@ -9083,6 +9273,7 @@ function updateDocumentControls() {
|
|||||||
|
|
||||||
function updateControls() {
|
function updateControls() {
|
||||||
renderSecurity();
|
renderSecurity();
|
||||||
|
updateStructureControls();
|
||||||
el.attachmentAdd.disabled = !state.editing || !state.ready || state.exporting;
|
el.attachmentAdd.disabled = !state.editing || !state.ready || state.exporting;
|
||||||
renderMetaContent(el.attachmentList, [state.doc?.attachments, state.doc?.attachmentError, state.editing], renderAttachments);
|
renderMetaContent(el.attachmentList, [state.doc?.attachments, state.doc?.attachmentError, state.editing], renderAttachments);
|
||||||
const hasDoc = Boolean(state.doc);
|
const hasDoc = Boolean(state.doc);
|
||||||
@@ -9289,6 +9480,7 @@ function showError(err, empty = !state.doc) {
|
|||||||
|
|
||||||
function setBusy(busy, text = "", percent = 0, status = "") {
|
function setBusy(busy, text = "", percent = 0, status = "") {
|
||||||
document.body.toggleAttribute("aria-busy", busy);
|
document.body.toggleAttribute("aria-busy", busy);
|
||||||
|
updateStructureControls();
|
||||||
updateDocumentControls();
|
updateDocumentControls();
|
||||||
renderSecurity();
|
renderSecurity();
|
||||||
updateDisplayControls();
|
updateDisplayControls();
|
||||||
@@ -9312,6 +9504,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
|||||||
el.objectStyleForm.inert = busy;
|
el.objectStyleForm.inert = busy;
|
||||||
el.objectBoundsForm.inert = busy;
|
el.objectBoundsForm.inert = busy;
|
||||||
el.outlineForm.inert = busy;
|
el.outlineForm.inert = busy;
|
||||||
|
el.structureForm.inert = busy;
|
||||||
el.outlineList.inert = busy;
|
el.outlineList.inert = busy;
|
||||||
el.navigationTabs.inert = busy;
|
el.navigationTabs.inert = busy;
|
||||||
el.pageList.inert = busy;
|
el.pageList.inert = busy;
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoRenderPage", renderPage)
|
registerCallback("ofdgoRenderPage", renderPage)
|
||||||
registerCallback("ofdgoOutlineLinks", outlineLinks)
|
registerCallback("ofdgoOutlineLinks", outlineLinks)
|
||||||
registerCallback("ofdgoStructureTargets", structureTargets)
|
registerCallback("ofdgoStructureTargets", structureTargets)
|
||||||
|
registerCallback("ofdgoStructureObjects", structureObjects)
|
||||||
|
registerCallback("ofdgoChangeStructure", changeStructure)
|
||||||
registerCallback("ofdgoOpenLinks", openLinks)
|
registerCallback("ofdgoOpenLinks", openLinks)
|
||||||
registerCallback("ofdgoRenderBackends", renderBackends)
|
registerCallback("ofdgoRenderBackends", renderBackends)
|
||||||
registerCallback("ofdgoSVGFontData", svgFontData)
|
registerCallback("ofdgoSVGFontData", svgFontData)
|
||||||
@@ -923,6 +925,57 @@ func outlineLinks(args []js.Value) (any, error) {
|
|||||||
return linkResults(links)
|
return linkResults(links)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// changeStructure 提交标引操作,XML、目标校验及历史维护均由库完成
|
||||||
|
// 入参: args JSON操作参数
|
||||||
|
// 返回: any 编辑状态, error 参数、校验或提交错误
|
||||||
|
func changeStructure(args []js.Value) (any, error) {
|
||||||
|
if len(args) != 1 || args[0].Type() != js.TypeString {
|
||||||
|
return nil, fmt.Errorf("invalid structure arguments")
|
||||||
|
}
|
||||||
|
var request struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
Path []int `json:"path"`
|
||||||
|
TypeID string `json:"typeId"`
|
||||||
|
Node ofdgo.StructureNode `json:"node"`
|
||||||
|
References []ofdgo.StructureReference `json:"references"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(args[0].String()), &request); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return changeObjects(func() error {
|
||||||
|
switch request.Action {
|
||||||
|
case "create":
|
||||||
|
_, err := currentEditor.AddStructure(ofdgo.CustomTag{TypeID: request.TypeID, NameSpace: request.Node.Namespace}, request.Node)
|
||||||
|
return err
|
||||||
|
case "add":
|
||||||
|
_, err := currentEditor.AddStructureNode(request.Index, request.Path, request.Node)
|
||||||
|
return err
|
||||||
|
case "update":
|
||||||
|
return currentEditor.UpdateStructureNode(request.Index, request.Path, request.Node)
|
||||||
|
case "delete":
|
||||||
|
if len(request.Path) == 0 {
|
||||||
|
return currentEditor.DeleteStructure(request.Index)
|
||||||
|
}
|
||||||
|
return currentEditor.DeleteStructureNode(request.Index, request.Path)
|
||||||
|
case "bind":
|
||||||
|
return currentEditor.SetStructureReferences(request.Index, request.Path, request.References)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported structure action %q", request.Action)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// structureObjects 获取当前编辑文档的绑定候选,不允许阅读模式调用编辑入口
|
||||||
|
// 入参: args 从0开始的页面索引
|
||||||
|
// 返回: any 图元标识和原文, error 参数或读取错误
|
||||||
|
func structureObjects(args []js.Value) (any, error) {
|
||||||
|
if currentEditor == nil || currentSession == nil || len(args) != 1 {
|
||||||
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
|
}
|
||||||
|
return currentSession.Renderer.StructureObjects(args[0].Int())
|
||||||
|
}
|
||||||
|
|
||||||
// structureTargets 查询当前文档标引节点的原文与定位范围
|
// structureTargets 查询当前文档标引节点的原文与定位范围
|
||||||
// 入参: args 标引清单索引和节点路径
|
// 入参: args 标引清单索引和节点路径
|
||||||
// 返回: any 标引目标, error 参数或解析错误
|
// 返回: any 标引目标, error 参数或解析错误
|
||||||
|
|||||||
@@ -0,0 +1,431 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Structure 获取当前编辑状态的标引,包括页面和图元删除后的引用调整
|
||||||
|
// 返回: []Structure 标引结构, error 快照或解析错误
|
||||||
|
func (e *Editor) Structure() ([]Structure, error) {
|
||||||
|
r, err := e.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
return r.Structure()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStructure 添加独立标引及根节点,FileLoc由库分配,不接收外部内容路径
|
||||||
|
// SchemaLoc仅允许引用包内已有文件,不访问网络或验证业务Schema
|
||||||
|
// 入参: tag 类型、命名空间及可选Schema位置, root 根节点
|
||||||
|
// 返回: int 从0开始的标引索引, error 校验或提交错误
|
||||||
|
func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) {
|
||||||
|
if strings.TrimSpace(tag.TypeID) == "" || tag.FileLoc != "" {
|
||||||
|
return 0, fmt.Errorf("custom tag requires TypeID and an automatically assigned FileLoc")
|
||||||
|
}
|
||||||
|
if !structureXMLText(tag.TypeID) || !structureXMLText(tag.NameSpace) || !structureXMLText(tag.SchemaLoc) {
|
||||||
|
return 0, fmt.Errorf("invalid custom tag metadata")
|
||||||
|
}
|
||||||
|
data, err := encodeStructureNode(root)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
tree, err := parseStructure(context.Background(), data, tag)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if err := e.validateStructureReferences(tree, tag); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
base := e.source
|
||||||
|
if base == nil {
|
||||||
|
base, err = e.importBase()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tag.SchemaLoc != "" {
|
||||||
|
name := base.reader.ResPath(tag.SchemaLoc)
|
||||||
|
if _, err := base.reader.readFile(name); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
tag.SchemaLoc = "/" + name
|
||||||
|
}
|
||||||
|
name, index, container, entries, err := editorStructureIndex(base)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
tag.FileLoc = "/" + e.packageName("Tags/Tag.xml")
|
||||||
|
var attrs ofdAttrs
|
||||||
|
attrs.add("TypeID", tag.TypeID)
|
||||||
|
attrs.add("NameSpace", tag.NameSpace)
|
||||||
|
var content []byte
|
||||||
|
if tag.SchemaLoc != "" {
|
||||||
|
content = append(content, container.textXML("SchemaLoc", tag.SchemaLoc)...)
|
||||||
|
}
|
||||||
|
content = append(content, container.textXML("FileLoc", tag.FileLoc)...)
|
||||||
|
entry, err := container.containerXML("CustomTag", attrs, content)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
parts := map[string][]byte{strings.TrimPrefix(tag.FileLoc, "/"): data}
|
||||||
|
if container.open != container.end {
|
||||||
|
entry = append(bytes.Clone(index[container.open:container.close]), entry...)
|
||||||
|
}
|
||||||
|
parts[name] = editorPatchXML(index, []editorXMLPatch{editorXMLContent(index, container, entry)})
|
||||||
|
if base.document.CustomTags.Path == "" && name != base.reader.documentRoot {
|
||||||
|
docName := base.reader.documentRoot
|
||||||
|
docData, err := base.reader.readFile(docName)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
doc, err := parseEditorXML(docData)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
parts[docName], err = editorXMLSetText(docData, doc, [][2]string{{"CustomTags", "/" + name}})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(entries), e.commitAnnotationParts(base, parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteStructure 删除标引入口,不删除正文或其他入口共享的资源
|
||||||
|
// 入参: index 从0开始的标引索引
|
||||||
|
// 返回: error 索引或提交错误
|
||||||
|
func (e *Editor) DeleteStructure(index int) error {
|
||||||
|
if e.source == nil {
|
||||||
|
return fmt.Errorf("custom tag structure not found: %d", index)
|
||||||
|
}
|
||||||
|
name, data, _, entries, err := editorStructureIndex(e.source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if index < 0 || index >= len(entries) {
|
||||||
|
return fmt.Errorf("custom tag structure not found: %d", index)
|
||||||
|
}
|
||||||
|
node := entries[index]
|
||||||
|
return e.commitAnnotationParts(e.source, map[string][]byte{name: editorPatchXML(data, []editorXMLPatch{{node.start, node.end, nil}})})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStructureNode 在指定父节点末尾添加子树,空路径表示根节点
|
||||||
|
// 入参: index 标引索引, parent 从0开始的节点路径, node 新节点及子树
|
||||||
|
// 返回: []int 新节点路径, error 校验或提交错误
|
||||||
|
func (e *Editor) AddStructureNode(index int, parent []int, node StructureNode) ([]int, error) {
|
||||||
|
encoded, err := encodeStructureNode(node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var result []int
|
||||||
|
err = e.editStructure(index, func(data []byte, root *editorXML, tag CustomTag) ([]byte, error) {
|
||||||
|
target, err := editorStructureNode(root, parent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if structureOfficial(tag, root.name.Space) && target.name.Space == root.name.Space && target.name.Local == "ObjectRef" {
|
||||||
|
return nil, fmt.Errorf("ObjectRef cannot contain child nodes")
|
||||||
|
}
|
||||||
|
content := bytes.Clone(encoded)
|
||||||
|
if target.open != target.end {
|
||||||
|
content = append(bytes.Clone(data[target.open:target.close]), content...)
|
||||||
|
}
|
||||||
|
result = append(slices.Clone(parent), len(target.children))
|
||||||
|
return editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, target, content)}), nil
|
||||||
|
})
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStructureNode 修改节点名称、命名空间、属性和直接文本,保留子节点、注释及处理指令
|
||||||
|
// node的Children和References必须为空,图元关联通过SetStructureReferences维护
|
||||||
|
// 入参: index 标引索引, path 从0开始的节点路径, node 节点字段
|
||||||
|
// 返回: error 校验或提交错误
|
||||||
|
func (e *Editor) UpdateStructureNode(index int, path []int, node StructureNode) error {
|
||||||
|
if len(node.Children) != 0 || len(node.References) != 0 {
|
||||||
|
return fmt.Errorf("use node operations to edit children or references")
|
||||||
|
}
|
||||||
|
return e.editStructure(index, func(data []byte, root *editorXML, _ CustomTag) ([]byte, error) {
|
||||||
|
target, err := editorStructureNode(root, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
updated, err := updateStructureNodeXML(data, target, node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return editorPatchXML(data, []editorXMLPatch{{target.start, target.end, updated}}), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteStructureNode 删除节点及其子树,根节点须通过DeleteStructure删除
|
||||||
|
// 入参: index 标引索引, path 从0开始的非空节点路径
|
||||||
|
// 返回: error 路径或提交错误
|
||||||
|
func (e *Editor) DeleteStructureNode(index int, path []int) error {
|
||||||
|
if len(path) == 0 {
|
||||||
|
return fmt.Errorf("delete the structure entry to remove its root")
|
||||||
|
}
|
||||||
|
return e.editStructure(index, func(data []byte, root *editorXML, _ CustomTag) ([]byte, error) {
|
||||||
|
node, err := editorStructureNode(root, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return editorPatchXML(data, []editorXMLPatch{{node.start, node.end, nil}}), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStructureReferences 替换公文节点的直接ObjectRef,保留其他子节点和原文
|
||||||
|
// 按输入顺序绑定图元,允许跨页和重复引用;空列表解除绑定,不修改版式内容
|
||||||
|
// 入参: index 标引索引, path 节点路径, references 页面标识和有序图元标识
|
||||||
|
// 返回: error 类型、目标校验或提交错误
|
||||||
|
func (e *Editor) SetStructureReferences(index int, path []int, references []StructureReference) error {
|
||||||
|
return e.editStructure(index, func(data []byte, root *editorXML, tag CustomTag) ([]byte, error) {
|
||||||
|
node, err := editorStructureNode(root, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !structureOfficial(tag, root.name.Space) || node.name.Local == "ObjectRef" {
|
||||||
|
return nil, fmt.Errorf("custom tag does not support ObjectRef binding")
|
||||||
|
}
|
||||||
|
for parent := node; parent != nil; parent = parent.parent {
|
||||||
|
if parent.name.Space != root.name.Space {
|
||||||
|
return nil, fmt.Errorf("cannot bind objects inside an extension namespace")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var added []byte
|
||||||
|
for _, ref := range references {
|
||||||
|
if editorResourceID(ref.PageID) == "" || len(ref.ObjectIDs) == 0 {
|
||||||
|
return nil, fmt.Errorf("ObjectRef requires a page and object identifiers")
|
||||||
|
}
|
||||||
|
for _, id := range ref.ObjectIDs {
|
||||||
|
if editorResourceID(id) == "" {
|
||||||
|
return nil, fmt.Errorf("invalid custom tag object identifier: %s", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoded, err := encodeStructureNode(StructureNode{Name: "ObjectRef", Namespace: root.name.Space, Attributes: []StructureAttribute{{Name: "PageRef", Value: ref.PageID}}, Text: strings.Join(ref.ObjectIDs, ",")})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
added = append(added, encoded...)
|
||||||
|
}
|
||||||
|
var content []byte
|
||||||
|
if node.open != node.end {
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
for _, child := range node.children {
|
||||||
|
if child.name.Space == root.name.Space && child.name.Local == "ObjectRef" {
|
||||||
|
patches = append(patches, editorXMLPatch{child.start - node.open, child.end - node.open, nil})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content = editorPatchXML(data[node.open:node.close], patches)
|
||||||
|
}
|
||||||
|
content = append(content, added...)
|
||||||
|
return editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, content)}), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorStructureIndex 读取或准备当前文档的标引索引,保留内联入口和未知扩展
|
||||||
|
// 入参: base 编辑包快照
|
||||||
|
// 返回: string 索引文件路径, []byte 原文, *editorXML 容器, []*editorXML 标引入口, error 读取错误
|
||||||
|
func editorStructureIndex(base *editorSource) (string, []byte, *editorXML, []*editorXML, error) {
|
||||||
|
name := base.reader.documentRoot
|
||||||
|
if base.document.CustomTags.Path != "" {
|
||||||
|
name = base.reader.ResPath(base.document.CustomTags.Path)
|
||||||
|
}
|
||||||
|
data, err := base.reader.readFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, nil, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, nil, err
|
||||||
|
}
|
||||||
|
if base.document.CustomTags.Path == "" {
|
||||||
|
container := root.child("CustomTags")
|
||||||
|
if container == nil || len(container.children) == 0 {
|
||||||
|
name = packageAvailableName(base.reader, nil, base.reader.RootDir+"/CustomTags.xml")
|
||||||
|
data, err = root.containerXML("CustomTags", nil, nil)
|
||||||
|
if err == nil {
|
||||||
|
root, err = parseEditorXML(data)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
root = container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var entries []*editorXML
|
||||||
|
for _, child := range root.children {
|
||||||
|
if child.matchesOFD("CustomTag") {
|
||||||
|
entries = append(entries, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name, data, root, entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorStructureNode 按完整XML子节点索引定位,与Reader.Structure的路径一致
|
||||||
|
// 入参: root 根节点, path 从0开始的路径
|
||||||
|
// 返回: *editorXML 节点, error 路径错误
|
||||||
|
func editorStructureNode(root *editorXML, path []int) (*editorXML, error) {
|
||||||
|
for _, index := range path {
|
||||||
|
if index < 0 || index >= len(root.children) {
|
||||||
|
return nil, fmt.Errorf("custom tag node not found")
|
||||||
|
}
|
||||||
|
root = root.children[index]
|
||||||
|
}
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// editStructure 验证完整标引后原子提交,隔离共享入口并保留未修改的XML
|
||||||
|
// 入参: index 标引索引, edit 内容变换
|
||||||
|
// 返回: error 校验、解析或提交错误
|
||||||
|
func (e *Editor) editStructure(index int, edit func([]byte, *editorXML, CustomTag) ([]byte, error)) error {
|
||||||
|
if e.source == nil {
|
||||||
|
return fmt.Errorf("custom tag structure not found: %d", index)
|
||||||
|
}
|
||||||
|
tags, err := e.source.reader.CustomTags()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if index < 0 || index >= len(tags) {
|
||||||
|
return fmt.Errorf("custom tag structure not found: %d", index)
|
||||||
|
}
|
||||||
|
tag := tags[index]
|
||||||
|
name := e.source.reader.ResPath(tag.FileLoc)
|
||||||
|
snapshot, err := e.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer snapshot.Close()
|
||||||
|
current, err := snapshot.CustomTags()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if index >= len(current) {
|
||||||
|
return fmt.Errorf("custom tag structure not found: %d", index)
|
||||||
|
}
|
||||||
|
data, err := snapshot.readFile(snapshot.ResPath(current[index].FileLoc))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := parseStructure(context.Background(), data, tag); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updated, err := edit(data, root, tag)
|
||||||
|
if err != nil || bytes.Equal(data, updated) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tree, err := parseStructure(context.Background(), updated, tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.validateStructureReferences(tree, tag); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parts := make(map[string][]byte)
|
||||||
|
for i, other := range tags {
|
||||||
|
if i != index && e.source.reader.ResPath(other.FileLoc) == name {
|
||||||
|
indexName, indexData, _, entries, err := editorStructureIndex(e.source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
name = e.packageName("Tags/Tag.xml")
|
||||||
|
loc := entries[index].child("FileLoc")
|
||||||
|
if loc == nil {
|
||||||
|
return fmt.Errorf("custom tag requires FileLoc")
|
||||||
|
}
|
||||||
|
var value bytes.Buffer
|
||||||
|
_ = xml.EscapeText(&value, []byte("/"+name))
|
||||||
|
parts[indexName] = editorPatchXML(indexData, []editorXMLPatch{editorXMLContent(indexData, loc, value.Bytes())})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts[name] = updated
|
||||||
|
return e.commitAnnotationParts(e.source, parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateStructureReferences 检查标引目标是否位于当前文档和版本,不依赖字体或图像解码
|
||||||
|
// 入参: root 待提交标引树, tag 标引入口
|
||||||
|
// 返回: error 页面或图元缺失错误
|
||||||
|
func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag) error {
|
||||||
|
var refs []StructureReference
|
||||||
|
var collect func(*StructureNode, bool) error
|
||||||
|
collect = func(node *StructureNode, official bool) error {
|
||||||
|
official = official && node.Namespace == root.Namespace
|
||||||
|
if official && node.Name == "ObjectRef" && len(node.Children) != 0 {
|
||||||
|
return fmt.Errorf("ObjectRef cannot contain child nodes")
|
||||||
|
}
|
||||||
|
refs = append(refs, node.References...)
|
||||||
|
for i := range node.Children {
|
||||||
|
if err := collect(&node.Children[i], official); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := collect(root, structureOfficial(tag, root.Namespace)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(refs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r, err := e.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
doc, err := r.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
renderer := e.newRenderer(r)
|
||||||
|
pages := signaturePageNumbers(doc)
|
||||||
|
checked := make(map[int]map[string]bool)
|
||||||
|
for _, ref := range refs {
|
||||||
|
page, _ := resourceValue(pages, ref.PageID)
|
||||||
|
if page == 0 {
|
||||||
|
return fmt.Errorf("custom tag page not found: %s", ref.PageID)
|
||||||
|
}
|
||||||
|
objects := checked[page]
|
||||||
|
if objects == nil {
|
||||||
|
content, err := r.PageContentByIndex(page - 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
visitor := &structureObjectVisitor{objects: make(map[string]bool)}
|
||||||
|
if err := renderer.WalkPage(content, visitor); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
objects = visitor.objects
|
||||||
|
checked[page] = objects
|
||||||
|
}
|
||||||
|
for _, id := range ref.ObjectIDs {
|
||||||
|
if !objects[editorResourceID(id)] {
|
||||||
|
return fmt.Errorf("custom tag object not found: %s/%s", ref.PageID, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -252,6 +252,15 @@ func editorXMLTextValue(data []byte, node *editorXML, before, after string) ([]b
|
|||||||
content.Reset()
|
content.Reset()
|
||||||
content.Write(escaped)
|
content.Write(escaped)
|
||||||
}
|
}
|
||||||
|
return editorXMLDirectText(data, node, content.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorXMLDirectText 替换直接文本,保留子节点、注释和处理指令
|
||||||
|
// 入参: data 原XML, node 目标节点, text 已转义的文本
|
||||||
|
// 返回: []byte 节点片段, error XML解析错误
|
||||||
|
func editorXMLDirectText(data []byte, node *editorXML, text []byte) ([]byte, error) {
|
||||||
|
var content bytes.Buffer
|
||||||
|
content.Write(text)
|
||||||
if node.open < node.close {
|
if node.open < node.close {
|
||||||
inner := data[node.open:node.close]
|
inner := data[node.open:node.close]
|
||||||
decoder := xml.NewDecoder(bytes.NewReader(inner))
|
decoder := xml.NewDecoder(bytes.NewReader(inner))
|
||||||
|
|||||||
@@ -30,10 +30,12 @@ const structureTypeOfficial = "od"
|
|||||||
const structureNamespaceEGov2013 = "http://www.nits.org.cn/eGovDoc/2013"
|
const structureNamespaceEGov2013 = "http://www.nits.org.cn/eGovDoc/2013"
|
||||||
|
|
||||||
// Structure 保存自定义标引入口及结构树,单个文件解析失败时通过Error报告,不影响其他标引
|
// Structure 保存自定义标引入口及结构树,单个文件解析失败时通过Error报告,不影响其他标引
|
||||||
|
// CanBind表示已识别公文引用语义,具体节点和目标仍由绑定接口校验
|
||||||
type Structure struct {
|
type Structure struct {
|
||||||
TypeID string `json:"typeId,omitempty"`
|
TypeID string `json:"typeId,omitempty"`
|
||||||
Namespace string `json:"namespace,omitempty"`
|
Namespace string `json:"namespace,omitempty"`
|
||||||
FileLoc string `json:"fileLoc"`
|
FileLoc string `json:"fileLoc"`
|
||||||
|
CanBind bool `json:"canBind,omitempty"`
|
||||||
Root *StructureNode `json:"root,omitempty"`
|
Root *StructureNode `json:"root,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -97,6 +99,9 @@ func (r *Reader) StructureContext(ctx context.Context) ([]Structure, error) {
|
|||||||
data, err := r.readFileViewContext(ctx, r.ResPath(tag.FileLoc))
|
data, err := r.readFileViewContext(ctx, r.ResPath(tag.FileLoc))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
item.Root, err = parseStructure(ctx, data, tag)
|
item.Root, err = parseStructure(ctx, data, tag)
|
||||||
|
if err == nil {
|
||||||
|
item.CanBind = structureOfficial(tag, item.Root.Namespace)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
// structureObjectVisitor 收集页面及复合图元标识,绑定校验不解码字体、图片或裁剪
|
||||||
|
type structureObjectVisitor struct {
|
||||||
|
objects map[string]bool
|
||||||
|
targets []StructureTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
// StructureObjects 列出指定页可绑定的图元及原文,包含模板、隐藏内容和复合图元成员
|
||||||
|
// 不解码字体和图片,返回顺序遵循页面遍历顺序,同一标识仅列出一次
|
||||||
|
// 入参: page 从0开始的页面索引
|
||||||
|
// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面或遍历错误
|
||||||
|
func (r *Renderer) StructureObjects(page int) ([]StructureTarget, error) {
|
||||||
|
content, err := r.Reader.PageContentByIndex(page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
visitor := &structureObjectVisitor{objects: make(map[string]bool)}
|
||||||
|
if err := r.WalkPage(content, visitor); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i := range visitor.targets {
|
||||||
|
visitor.targets[i].Page, visitor.targets[i].PageID = page+1, content.ID
|
||||||
|
}
|
||||||
|
return visitor.targets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeginObject 登记图元标识,不创建分组状态
|
||||||
|
// 入参: object 页面图元
|
||||||
|
// 返回: bool 始终为false
|
||||||
|
func (v *structureObjectVisitor) BeginObject(object *GraphicObject) bool {
|
||||||
|
id := editorResourceID(editorObjectID(*object))
|
||||||
|
if id != "" && !v.objects[id] {
|
||||||
|
v.objects[id] = true
|
||||||
|
target := StructureTarget{ID: id}
|
||||||
|
if object.Type == "TextObject" {
|
||||||
|
target.Text = object.TextObject.Text()
|
||||||
|
}
|
||||||
|
v.targets = append(v.targets, target)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndObject 满足分组访问接口,不维护分组栈
|
||||||
|
func (v *structureObjectVisitor) EndObject() {}
|
||||||
|
|
||||||
|
// BeginAnnotation 不为注解容器分配正文图元标识
|
||||||
|
// 入参: id 注解标识
|
||||||
|
// 返回: bool 始终为false
|
||||||
|
func (v *structureObjectVisitor) BeginAnnotation(id string) bool { return false }
|
||||||
|
|
||||||
|
// SkipObjects 不依赖图元序号
|
||||||
|
// 入参: count 跳过数量
|
||||||
|
func (v *structureObjectVisitor) SkipObjects(count int) {}
|
||||||
|
|
||||||
|
// IncludeHidden 允许绑定隐藏图元
|
||||||
|
// 返回: bool 始终为true
|
||||||
|
func (v *structureObjectVisitor) IncludeHidden() bool { return true }
|
||||||
|
|
||||||
|
// SkipClips 绑定校验不需要裁剪轮廓
|
||||||
|
// 返回: bool 始终为true
|
||||||
|
func (v *structureObjectVisitor) SkipClips() bool { return true }
|
||||||
|
|
||||||
|
// DrawObject 不绘制图元,标识已由BeginObject登记
|
||||||
|
// 入参: object 页面图元, state 继承状态
|
||||||
|
// 返回: error 始终为nil
|
||||||
|
func (v *structureObjectVisitor) DrawObject(object *GraphicObject, state RenderState) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawStamp 不将签章外观作为正文绑定目标
|
||||||
|
// 入参: stamp 签章外观
|
||||||
|
// 返回: error 始终为nil
|
||||||
|
func (v *structureObjectVisitor) DrawStamp(stamp Stamp) error { return nil }
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encodeStructureNode 编码有界标引子树,引用由ObjectRef节点表达,不重复编码派生References字段
|
||||||
|
// 入参: node 根节点
|
||||||
|
// 返回: []byte XML片段, error 名称、属性或编码错误
|
||||||
|
func encodeStructureNode(node StructureNode) ([]byte, error) {
|
||||||
|
var output bytes.Buffer
|
||||||
|
encoder := xml.NewEncoder(&output)
|
||||||
|
count := 0
|
||||||
|
var write func(StructureNode, int) error
|
||||||
|
write = func(node StructureNode, depth int) error {
|
||||||
|
count++
|
||||||
|
if depth > 128 || count > 100000 {
|
||||||
|
return fmt.Errorf("custom tag structure limit exceeded")
|
||||||
|
}
|
||||||
|
if err := validateStructureNode(node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
start := xml.StartElement{Name: xml.Name{Space: node.Namespace, Local: node.Name}}
|
||||||
|
if node.Namespace == "" {
|
||||||
|
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: ""})
|
||||||
|
}
|
||||||
|
for _, attr := range node.Attributes {
|
||||||
|
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Space: attr.Namespace, Local: attr.Name}, Value: attr.Value})
|
||||||
|
}
|
||||||
|
if err := encoder.EncodeToken(start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := encoder.EncodeToken(xml.CharData(node.Text)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, child := range node.Children {
|
||||||
|
if err := write(child, depth+1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encoder.EncodeToken(start.End())
|
||||||
|
}
|
||||||
|
if err := write(node, 1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := encoder.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return output.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateStructureNode 校验XML名称和文本,禁止重复属性及伪造命名空间声明
|
||||||
|
// 入参: node 节点字段
|
||||||
|
// 返回: error 字段校验错误
|
||||||
|
func validateStructureNode(node StructureNode) error {
|
||||||
|
if !ofdXMLIDValid(node.Name) || node.Name == "xmlns" {
|
||||||
|
return fmt.Errorf("invalid custom tag node name: %s", node.Name)
|
||||||
|
}
|
||||||
|
if !structureXMLText(node.Text) || !structureXMLText(node.Namespace) || node.Namespace == "http://www.w3.org/2000/xmlns/" {
|
||||||
|
return fmt.Errorf("invalid custom tag text or namespace")
|
||||||
|
}
|
||||||
|
seen := make(map[xml.Name]bool)
|
||||||
|
for _, attr := range node.Attributes {
|
||||||
|
name := xml.Name{Space: attr.Namespace, Local: attr.Name}
|
||||||
|
if !ofdXMLIDValid(attr.Name) || attr.Name == "xmlns" || attr.Namespace == "http://www.w3.org/2000/xmlns/" || !structureXMLText(attr.Namespace) || !structureXMLText(attr.Value) || seen[name] {
|
||||||
|
return fmt.Errorf("invalid or duplicate custom tag attribute: %s", attr.Name)
|
||||||
|
}
|
||||||
|
seen[name] = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// structureXMLText 检查XML 1.0允许的字符,不以替换字符掩盖无效输入
|
||||||
|
// 入参: value 文本
|
||||||
|
// 返回: bool 是否可无损编码
|
||||||
|
func structureXMLText(value string) bool {
|
||||||
|
if !utf8.ValidString(value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range value {
|
||||||
|
if r != '\t' && r != '\n' && r != '\r' && (r < 0x20 || r == 0xfffe || r == 0xffff) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateStructureNodeXML 替换节点字段,保留子树继承的命名空间和非文本XML内容
|
||||||
|
// 入参: data 原XML, source 原节点, node 新字段
|
||||||
|
// 返回: []byte 独立节点片段, error 校验或编码错误
|
||||||
|
func updateStructureNodeXML(data []byte, source *editorXML, node StructureNode) ([]byte, error) {
|
||||||
|
if err := validateStructureNode(node); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fragment, err := editorXMLStandalone(data[source.start:source.end], source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
before, err := parseStructure(context.Background(), fragment, CustomTag{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if node.Name == before.Name && node.Namespace == before.Namespace && node.Text == before.Text && slices.Equal(node.Attributes, before.Attributes) {
|
||||||
|
return data[source.start:source.end], nil
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(fragment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if before.Text != node.Text {
|
||||||
|
var text bytes.Buffer
|
||||||
|
if err := xml.EscapeText(&text, []byte(node.Text)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fragment, err = editorXMLDirectText(fragment, root, text.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root, err = parseEditorXML(fragment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var attrs []xml.Attr
|
||||||
|
used := make(map[string]bool)
|
||||||
|
for _, attr := range root.attrs {
|
||||||
|
if attr.Name.Space == "xmlns" {
|
||||||
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "xmlns:" + attr.Name.Local}, Value: attr.Value})
|
||||||
|
used[attr.Name.Local] = true
|
||||||
|
} else if attr.Name.Space == "" && attr.Name.Local == "xmlns" {
|
||||||
|
attrs = append(attrs, attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qualify := func(name, namespace string) string {
|
||||||
|
if namespace == "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
if namespace == "http://www.w3.org/XML/1998/namespace" {
|
||||||
|
return "xml:" + name
|
||||||
|
}
|
||||||
|
for _, attr := range attrs {
|
||||||
|
if strings.HasPrefix(attr.Name.Local, "xmlns:") && attr.Value == namespace {
|
||||||
|
return strings.TrimPrefix(attr.Name.Local, "xmlns:") + ":" + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefix := "tag"
|
||||||
|
for i := 1; used[prefix]; i++ {
|
||||||
|
prefix = "tag" + strconv.Itoa(i)
|
||||||
|
}
|
||||||
|
used[prefix] = true
|
||||||
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "xmlns:" + prefix}, Value: namespace})
|
||||||
|
return prefix + ":" + name
|
||||||
|
}
|
||||||
|
name := qualify(node.Name, node.Namespace)
|
||||||
|
if node.Namespace == "" {
|
||||||
|
for i := range attrs {
|
||||||
|
if attrs[i].Name.Local == "xmlns" {
|
||||||
|
attrs[i].Value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, attr := range node.Attributes {
|
||||||
|
key := qualify(attr.Name, attr.Namespace)
|
||||||
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: key}, Value: attr.Value})
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
encoder := xml.NewEncoder(&output)
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: name}, Attr: attrs}
|
||||||
|
if err := encoder.EncodeToken(start); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := encoder.Flush(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if root.open != root.end {
|
||||||
|
if node.Namespace == "" && before.Namespace != "" {
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
for _, child := range root.children {
|
||||||
|
standalone, err := editorXMLStandalone(fragment[child.start:child.end], child)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
patches = append(patches, editorXMLPatch{child.start - root.open, child.end - root.open, standalone})
|
||||||
|
}
|
||||||
|
output.Write(editorPatchXML(fragment[root.open:root.close], patches))
|
||||||
|
} else {
|
||||||
|
output.Write(fragment[root.open:root.close])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := encoder.EncodeToken(start.End()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := encoder.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return output.Bytes(), nil
|
||||||
|
}
|
||||||
Reference in new issue
Block a user