python-hwpx-automation 6.0.3__py3-none-any.whl
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.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
from typing import Any, Sequence
|
|
9
|
+
from xml.etree import ElementTree as ET
|
|
10
|
+
|
|
11
|
+
from hwpx.opc.package import HwpxPackage
|
|
12
|
+
from hwpx.opc.relationships import parse_manifest_relationships
|
|
13
|
+
from hwpx.tools.archive_cli import unpack_hwpx
|
|
14
|
+
|
|
15
|
+
from ..quality import DocumentMetrics, collect_metrics
|
|
16
|
+
|
|
17
|
+
_HH_NS = "http://www.hancom.co.kr/hwpml/2011/head"
|
|
18
|
+
_HP_NS = "http://www.hancom.co.kr/hwpml/2011/paragraph"
|
|
19
|
+
_HP_TAG = f"{{{_HP_NS}}}"
|
|
20
|
+
_NS = {"hh": _HH_NS, "hp": _HP_NS}
|
|
21
|
+
TEMPLATE_ANALYSIS_SCHEMA_VERSION = "hwpx.template-analysis.v2"
|
|
22
|
+
TEMPLATE_ANALYSIS_AGENT_SCHEMA_VERSION = "hwpx.template-analysis.agent-schema.v1"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"CellSummary",
|
|
26
|
+
"CharPropertySummary",
|
|
27
|
+
"ColumnWidthSummary",
|
|
28
|
+
"FontFaceSummary",
|
|
29
|
+
"HeaderSummary",
|
|
30
|
+
"RunStyleReference",
|
|
31
|
+
"SectionLayoutSummary",
|
|
32
|
+
"TableSummary",
|
|
33
|
+
"TemplateAnalysis",
|
|
34
|
+
"analyze_template",
|
|
35
|
+
"extract_template_parts",
|
|
36
|
+
"main",
|
|
37
|
+
"template_analysis_agent_schema",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class HeaderSummary:
|
|
43
|
+
font_count: int
|
|
44
|
+
char_pr_count: int
|
|
45
|
+
para_pr_count: int
|
|
46
|
+
border_fill_count: int
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class FontFaceSummary:
|
|
51
|
+
lang: str
|
|
52
|
+
fonts: tuple[dict[str, str], ...]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class CharPropertySummary:
|
|
57
|
+
id: str
|
|
58
|
+
attributes: dict[str, str]
|
|
59
|
+
flags: dict[str, bool]
|
|
60
|
+
font_ref: dict[str, dict[str, str | None]]
|
|
61
|
+
human_readable: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class RunStyleReference:
|
|
66
|
+
section_path: str
|
|
67
|
+
run_index: int
|
|
68
|
+
char_pr_id_ref: str | None
|
|
69
|
+
text: str
|
|
70
|
+
style: dict[str, Any] | None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class SectionLayoutSummary:
|
|
75
|
+
section_path: str
|
|
76
|
+
page_width: int | None
|
|
77
|
+
page_height: int | None
|
|
78
|
+
margins: dict[str, int]
|
|
79
|
+
computed_body_width: int | None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class ColumnWidthSummary:
|
|
84
|
+
widths: tuple[int | None, ...]
|
|
85
|
+
complete: bool
|
|
86
|
+
source_cell_count: int
|
|
87
|
+
skipped_colspan_cell_count: int
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class CellSummary:
|
|
92
|
+
row: int
|
|
93
|
+
col: int
|
|
94
|
+
row_span: int
|
|
95
|
+
col_span: int
|
|
96
|
+
width: int | None
|
|
97
|
+
height: int | None
|
|
98
|
+
margin: dict[str, int]
|
|
99
|
+
vert_align: str | None
|
|
100
|
+
char_pr_id_refs: tuple[str, ...]
|
|
101
|
+
runs: tuple[dict[str, Any], ...]
|
|
102
|
+
text: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class TableSummary:
|
|
107
|
+
section_path: str
|
|
108
|
+
table_index: int
|
|
109
|
+
row_count: int
|
|
110
|
+
column_count: int
|
|
111
|
+
column_widths: ColumnWidthSummary
|
|
112
|
+
cells: tuple[CellSummary, ...]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True)
|
|
116
|
+
class TemplateAnalysis:
|
|
117
|
+
schema_version: str
|
|
118
|
+
source_name: str
|
|
119
|
+
part_names: tuple[str, ...]
|
|
120
|
+
rootfiles: tuple[str, ...]
|
|
121
|
+
manifest_path: str
|
|
122
|
+
manifest_item_paths: tuple[str, ...]
|
|
123
|
+
header_paths: tuple[str, ...]
|
|
124
|
+
section_paths: tuple[str, ...]
|
|
125
|
+
master_page_paths: tuple[str, ...]
|
|
126
|
+
history_paths: tuple[str, ...]
|
|
127
|
+
bin_data_paths: tuple[str, ...]
|
|
128
|
+
version_path: str | None
|
|
129
|
+
header_summary: HeaderSummary
|
|
130
|
+
font_faces: tuple[FontFaceSummary, ...]
|
|
131
|
+
char_properties: tuple[CharPropertySummary, ...]
|
|
132
|
+
section_layouts: tuple[SectionLayoutSummary, ...]
|
|
133
|
+
table_summaries: tuple[TableSummary, ...]
|
|
134
|
+
run_style_references: tuple[RunStyleReference, ...]
|
|
135
|
+
proxy_metrics: DocumentMetrics
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _summarize_header(element: ET.Element | None) -> HeaderSummary:
|
|
139
|
+
if element is None:
|
|
140
|
+
return HeaderSummary(font_count=0, char_pr_count=0, para_pr_count=0, border_fill_count=0)
|
|
141
|
+
|
|
142
|
+
font_count = len(element.findall(".//hh:fontface/hh:font", _NS))
|
|
143
|
+
char_pr_count = len(element.findall(".//hh:charPr", _NS))
|
|
144
|
+
para_pr_count = len(element.findall(".//hh:paraPr", _NS))
|
|
145
|
+
border_fill_count = len(element.findall(".//hh:borderFill", _NS))
|
|
146
|
+
return HeaderSummary(
|
|
147
|
+
font_count=font_count,
|
|
148
|
+
char_pr_count=char_pr_count,
|
|
149
|
+
para_pr_count=para_pr_count,
|
|
150
|
+
border_fill_count=border_fill_count,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def template_analysis_agent_schema() -> dict[str, Any]:
|
|
155
|
+
return {
|
|
156
|
+
"schemaVersion": TEMPLATE_ANALYSIS_AGENT_SCHEMA_VERSION,
|
|
157
|
+
"analysisSchemaVersion": TEMPLATE_ANALYSIS_SCHEMA_VERSION,
|
|
158
|
+
"description": "Agent-oriented fields for template form-fit planning.",
|
|
159
|
+
"fieldGuide": {
|
|
160
|
+
"section_layouts[].computed_body_width": "Writable body width in HWP units after page margins and gutter.",
|
|
161
|
+
"table_summaries[].column_widths.widths": "Per-column widths reconstructed only from non-colspan cells.",
|
|
162
|
+
"table_summaries[].cells[].margin": "Cell margin attributes in HWP units.",
|
|
163
|
+
"table_summaries[].cells[].vert_align": "Cell subList vertical alignment.",
|
|
164
|
+
"table_summaries[].cells[].runs[].style": "Run charPrIDRef resolved to decoded style flags and font faces.",
|
|
165
|
+
"char_properties[].flags": "Decoded bold, italic, underline, and strikeout booleans.",
|
|
166
|
+
},
|
|
167
|
+
"recommendedUse": [
|
|
168
|
+
"Choose target cells by row/col and span before planning replacements.",
|
|
169
|
+
"Use computed_body_width and column_widths to size generated tables.",
|
|
170
|
+
"Preserve run charPrIDRef when replacing text in styled cells.",
|
|
171
|
+
],
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _int_attr(element: ET.Element | None, name: str, default: int | None = None) -> int | None:
|
|
176
|
+
if element is None:
|
|
177
|
+
return default
|
|
178
|
+
value = element.get(name)
|
|
179
|
+
if value is None:
|
|
180
|
+
return default
|
|
181
|
+
try:
|
|
182
|
+
return int(value)
|
|
183
|
+
except ValueError:
|
|
184
|
+
return default
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _element_text(element: ET.Element) -> str:
|
|
188
|
+
return "".join(text.text or "" for text in element.findall(f".//{_HP_TAG}t"))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _truncated_text(text: str, limit: int = 120) -> str:
|
|
192
|
+
if len(text) <= limit:
|
|
193
|
+
return text
|
|
194
|
+
return text[: limit - 1] + "…"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _extract_font_faces(header_xml: ET.Element | None) -> tuple[FontFaceSummary, ...]:
|
|
198
|
+
if header_xml is None:
|
|
199
|
+
return ()
|
|
200
|
+
summaries: list[FontFaceSummary] = []
|
|
201
|
+
for fontface in header_xml.findall(".//hh:fontface", _NS):
|
|
202
|
+
lang = (fontface.get("lang") or "").lower()
|
|
203
|
+
fonts: list[dict[str, str]] = []
|
|
204
|
+
for font in fontface.findall("hh:font", _NS):
|
|
205
|
+
fonts.append({key: value for key, value in font.attrib.items()})
|
|
206
|
+
summaries.append(FontFaceSummary(lang=lang, fonts=tuple(fonts)))
|
|
207
|
+
return tuple(summaries)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _font_lookup(font_faces: Sequence[FontFaceSummary]) -> dict[str, dict[str, str]]:
|
|
211
|
+
lookup: dict[str, dict[str, str]] = {}
|
|
212
|
+
for face in font_faces:
|
|
213
|
+
lang_fonts = lookup.setdefault(face.lang, {})
|
|
214
|
+
for font in face.fonts:
|
|
215
|
+
font_id = font.get("id")
|
|
216
|
+
face_name = font.get("face") or font.get("name")
|
|
217
|
+
if font_id is not None and face_name is not None:
|
|
218
|
+
lang_fonts[font_id] = face_name
|
|
219
|
+
return lookup
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _active_style_flags(char_pr: ET.Element) -> dict[str, bool]:
|
|
223
|
+
underline = char_pr.find("hh:underline", _NS)
|
|
224
|
+
strikeout = char_pr.find("hh:strikeout", _NS)
|
|
225
|
+
return {
|
|
226
|
+
"bold": char_pr.find("hh:bold", _NS) is not None,
|
|
227
|
+
"italic": char_pr.find("hh:italic", _NS) is not None,
|
|
228
|
+
"underline": underline is not None and (underline.get("type") or "SOLID").upper() != "NONE",
|
|
229
|
+
"strikeout": strikeout is not None and (strikeout.get("shape") or "SOLID").upper() != "NONE",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _font_ref_summary(
|
|
234
|
+
char_pr: ET.Element,
|
|
235
|
+
font_faces: dict[str, dict[str, str]],
|
|
236
|
+
) -> dict[str, dict[str, str | None]]:
|
|
237
|
+
font_ref = char_pr.find("hh:fontRef", _NS)
|
|
238
|
+
if font_ref is None:
|
|
239
|
+
return {}
|
|
240
|
+
summary: dict[str, dict[str, str | None]] = {}
|
|
241
|
+
for lang, font_id in sorted(font_ref.attrib.items()):
|
|
242
|
+
summary[lang] = {
|
|
243
|
+
"id": font_id,
|
|
244
|
+
"face": font_faces.get(lang.lower(), {}).get(font_id),
|
|
245
|
+
}
|
|
246
|
+
return summary
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _human_style(flags: dict[str, bool], font_ref: dict[str, dict[str, str | None]]) -> str:
|
|
250
|
+
parts = [name for name in ("bold", "italic", "underline", "strikeout") if flags.get(name)]
|
|
251
|
+
faces = sorted({str(item["face"]) for item in font_ref.values() if item.get("face")})
|
|
252
|
+
if faces:
|
|
253
|
+
parts.append("font=" + ",".join(faces))
|
|
254
|
+
return ", ".join(parts) if parts else "regular"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _extract_char_properties(
|
|
258
|
+
header_xml: ET.Element | None,
|
|
259
|
+
font_faces: Sequence[FontFaceSummary],
|
|
260
|
+
) -> tuple[CharPropertySummary, ...]:
|
|
261
|
+
if header_xml is None:
|
|
262
|
+
return ()
|
|
263
|
+
fonts = _font_lookup(font_faces)
|
|
264
|
+
summaries: list[CharPropertySummary] = []
|
|
265
|
+
for char_pr in header_xml.findall(".//hh:charPr", _NS):
|
|
266
|
+
char_id = char_pr.get("id")
|
|
267
|
+
if char_id is None:
|
|
268
|
+
continue
|
|
269
|
+
flags = _active_style_flags(char_pr)
|
|
270
|
+
font_ref = _font_ref_summary(char_pr, fonts)
|
|
271
|
+
summaries.append(
|
|
272
|
+
CharPropertySummary(
|
|
273
|
+
id=char_id,
|
|
274
|
+
attributes={key: value for key, value in char_pr.attrib.items()},
|
|
275
|
+
flags=flags,
|
|
276
|
+
font_ref=font_ref,
|
|
277
|
+
human_readable=_human_style(flags, font_ref),
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
return tuple(summaries)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _style_lookup(char_properties: Sequence[CharPropertySummary]) -> dict[str, dict[str, Any]]:
|
|
284
|
+
return {
|
|
285
|
+
item.id: {
|
|
286
|
+
"id": item.id,
|
|
287
|
+
"flags": item.flags,
|
|
288
|
+
"fontRef": item.font_ref,
|
|
289
|
+
"humanReadable": item.human_readable,
|
|
290
|
+
}
|
|
291
|
+
for item in char_properties
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _run_summary(run: ET.Element, style_by_id: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
296
|
+
char_ref = run.get("charPrIDRef")
|
|
297
|
+
return {
|
|
298
|
+
"charPrIDRef": char_ref,
|
|
299
|
+
"text": _truncated_text(_element_text(run)),
|
|
300
|
+
"style": style_by_id.get(char_ref or ""),
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _extract_run_style_references(
|
|
305
|
+
section_path: str,
|
|
306
|
+
section_xml: ET.Element,
|
|
307
|
+
style_by_id: dict[str, dict[str, Any]],
|
|
308
|
+
) -> tuple[RunStyleReference, ...]:
|
|
309
|
+
refs: list[RunStyleReference] = []
|
|
310
|
+
for index, run in enumerate(section_xml.findall(f".//{_HP_TAG}run")):
|
|
311
|
+
char_ref = run.get("charPrIDRef")
|
|
312
|
+
refs.append(
|
|
313
|
+
RunStyleReference(
|
|
314
|
+
section_path=section_path,
|
|
315
|
+
run_index=index,
|
|
316
|
+
char_pr_id_ref=char_ref,
|
|
317
|
+
text=_truncated_text(_element_text(run)),
|
|
318
|
+
style=style_by_id.get(char_ref or ""),
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
return tuple(refs)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _margin_summary(element: ET.Element | None) -> dict[str, int]:
|
|
325
|
+
return {
|
|
326
|
+
name: _int_attr(element, name, 0) or 0
|
|
327
|
+
for name in ("left", "right", "top", "bottom", "header", "footer", "gutter")
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _extract_section_layout(section_path: str, section_xml: ET.Element) -> SectionLayoutSummary:
|
|
332
|
+
page_pr = section_xml.find(f".//{_HP_TAG}pagePr")
|
|
333
|
+
margin = page_pr.find(f"{_HP_TAG}margin") if page_pr is not None else None
|
|
334
|
+
margins = _margin_summary(margin)
|
|
335
|
+
page_width = _int_attr(page_pr, "width")
|
|
336
|
+
body_width = None
|
|
337
|
+
if page_width is not None:
|
|
338
|
+
body_width = max(
|
|
339
|
+
page_width
|
|
340
|
+
- margins.get("left", 0)
|
|
341
|
+
- margins.get("right", 0)
|
|
342
|
+
- margins.get("gutter", 0),
|
|
343
|
+
0,
|
|
344
|
+
)
|
|
345
|
+
return SectionLayoutSummary(
|
|
346
|
+
section_path=section_path,
|
|
347
|
+
page_width=page_width,
|
|
348
|
+
page_height=_int_attr(page_pr, "height"),
|
|
349
|
+
margins=margins,
|
|
350
|
+
computed_body_width=body_width,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _cell_int(cell: ET.Element, child_name: str, attr: str, default: int | None = None) -> int | None:
|
|
355
|
+
return _int_attr(cell.find(f"{_HP_TAG}{child_name}"), attr, default)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _cell_summary(
|
|
359
|
+
row_index: int,
|
|
360
|
+
cell_index: int,
|
|
361
|
+
cell: ET.Element,
|
|
362
|
+
style_by_id: dict[str, dict[str, Any]],
|
|
363
|
+
) -> CellSummary:
|
|
364
|
+
addr = cell.find(f"{_HP_TAG}cellAddr")
|
|
365
|
+
span = cell.find(f"{_HP_TAG}cellSpan")
|
|
366
|
+
margin = cell.find(f"{_HP_TAG}cellMargin")
|
|
367
|
+
sublist = cell.find(f"{_HP_TAG}subList")
|
|
368
|
+
runs = tuple(_run_summary(run, style_by_id) for run in cell.findall(f".//{_HP_TAG}run"))
|
|
369
|
+
char_refs = tuple(
|
|
370
|
+
sorted({str(run["charPrIDRef"]) for run in runs if run.get("charPrIDRef") is not None})
|
|
371
|
+
)
|
|
372
|
+
return CellSummary(
|
|
373
|
+
row=_int_attr(addr, "rowAddr", row_index) or row_index,
|
|
374
|
+
col=_int_attr(addr, "colAddr", cell_index) or cell_index,
|
|
375
|
+
row_span=_int_attr(span, "rowSpan", 1) or 1,
|
|
376
|
+
col_span=_int_attr(span, "colSpan", 1) or 1,
|
|
377
|
+
width=_cell_int(cell, "cellSz", "width"),
|
|
378
|
+
height=_cell_int(cell, "cellSz", "height"),
|
|
379
|
+
margin={
|
|
380
|
+
name: _int_attr(margin, name, 0) or 0
|
|
381
|
+
for name in ("left", "right", "top", "bottom")
|
|
382
|
+
},
|
|
383
|
+
vert_align=sublist.get("vertAlign") if sublist is not None else None,
|
|
384
|
+
char_pr_id_refs=char_refs,
|
|
385
|
+
runs=runs,
|
|
386
|
+
text=_truncated_text(_element_text(cell)),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _column_width_summary(cells: Sequence[CellSummary], column_count: int) -> ColumnWidthSummary:
|
|
391
|
+
widths: list[int | None] = [None] * max(column_count, 0)
|
|
392
|
+
source_cell_count = 0
|
|
393
|
+
skipped_colspan_cell_count = 0
|
|
394
|
+
for cell in cells:
|
|
395
|
+
if cell.col_span != 1:
|
|
396
|
+
skipped_colspan_cell_count += 1
|
|
397
|
+
continue
|
|
398
|
+
if cell.width is None or cell.width <= 0:
|
|
399
|
+
continue
|
|
400
|
+
if 0 <= cell.col < len(widths) and widths[cell.col] is None:
|
|
401
|
+
widths[cell.col] = cell.width
|
|
402
|
+
source_cell_count += 1
|
|
403
|
+
return ColumnWidthSummary(
|
|
404
|
+
widths=tuple(widths),
|
|
405
|
+
complete=all(width is not None for width in widths),
|
|
406
|
+
source_cell_count=source_cell_count,
|
|
407
|
+
skipped_colspan_cell_count=skipped_colspan_cell_count,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _extract_tables(
|
|
412
|
+
section_path: str,
|
|
413
|
+
section_xml: ET.Element,
|
|
414
|
+
style_by_id: dict[str, dict[str, Any]],
|
|
415
|
+
) -> tuple[TableSummary, ...]:
|
|
416
|
+
tables: list[TableSummary] = []
|
|
417
|
+
for table_index, table in enumerate(section_xml.findall(f".//{_HP_TAG}tbl")):
|
|
418
|
+
cells: list[CellSummary] = []
|
|
419
|
+
for row_index, row in enumerate(table.findall(f"{_HP_TAG}tr")):
|
|
420
|
+
for cell_index, cell in enumerate(row.findall(f"{_HP_TAG}tc")):
|
|
421
|
+
cells.append(_cell_summary(row_index, cell_index, cell, style_by_id))
|
|
422
|
+
column_count = _int_attr(table, "colCnt", 0) or 0
|
|
423
|
+
if column_count <= 0 and cells:
|
|
424
|
+
column_count = max(cell.col + max(cell.col_span, 1) for cell in cells)
|
|
425
|
+
row_count = _int_attr(table, "rowCnt", 0) or 0
|
|
426
|
+
if row_count <= 0 and cells:
|
|
427
|
+
row_count = max(cell.row + max(cell.row_span, 1) for cell in cells)
|
|
428
|
+
tables.append(
|
|
429
|
+
TableSummary(
|
|
430
|
+
section_path=section_path,
|
|
431
|
+
table_index=table_index,
|
|
432
|
+
row_count=row_count,
|
|
433
|
+
column_count=column_count,
|
|
434
|
+
column_widths=_column_width_summary(cells, column_count),
|
|
435
|
+
cells=tuple(cells),
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
return tuple(tables)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _is_bindata_path(path: str) -> bool:
|
|
442
|
+
return any(part.lower() == "bindata" for part in PurePosixPath(path).parts)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def analyze_template(source: str | Path) -> TemplateAnalysis:
|
|
446
|
+
source_path = Path(source)
|
|
447
|
+
package = HwpxPackage.open(source_path)
|
|
448
|
+
relationships = parse_manifest_relationships(
|
|
449
|
+
package.manifest_tree(),
|
|
450
|
+
package.main_content.full_path,
|
|
451
|
+
known_parts=package.part_names(),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
header_paths = tuple(package.header_paths())
|
|
455
|
+
header_xml = package.get_xml(header_paths[0]) if header_paths else None
|
|
456
|
+
font_faces = _extract_font_faces(header_xml)
|
|
457
|
+
char_properties = _extract_char_properties(header_xml, font_faces)
|
|
458
|
+
style_by_id = _style_lookup(char_properties)
|
|
459
|
+
|
|
460
|
+
section_layouts: list[SectionLayoutSummary] = []
|
|
461
|
+
table_summaries: list[TableSummary] = []
|
|
462
|
+
run_style_references: list[RunStyleReference] = []
|
|
463
|
+
section_paths = tuple(package.section_paths())
|
|
464
|
+
for section_path in section_paths:
|
|
465
|
+
section_xml = package.get_xml(section_path)
|
|
466
|
+
section_layouts.append(_extract_section_layout(section_path, section_xml))
|
|
467
|
+
table_summaries.extend(_extract_tables(section_path, section_xml, style_by_id))
|
|
468
|
+
run_style_references.extend(_extract_run_style_references(section_path, section_xml, style_by_id))
|
|
469
|
+
|
|
470
|
+
return TemplateAnalysis(
|
|
471
|
+
schema_version=TEMPLATE_ANALYSIS_SCHEMA_VERSION,
|
|
472
|
+
source_name=source_path.name,
|
|
473
|
+
part_names=tuple(package.part_names()),
|
|
474
|
+
rootfiles=tuple(rootfile.full_path for rootfile in package.iter_rootfiles()),
|
|
475
|
+
manifest_path=package.main_content.full_path,
|
|
476
|
+
manifest_item_paths=tuple(item.resolved_path for item in relationships.items),
|
|
477
|
+
header_paths=header_paths,
|
|
478
|
+
section_paths=section_paths,
|
|
479
|
+
master_page_paths=tuple(package.master_page_paths()),
|
|
480
|
+
history_paths=tuple(package.history_paths()),
|
|
481
|
+
bin_data_paths=tuple(
|
|
482
|
+
item.resolved_path for item in relationships.items if _is_bindata_path(item.resolved_path)
|
|
483
|
+
),
|
|
484
|
+
version_path=package.version_path(),
|
|
485
|
+
header_summary=_summarize_header(header_xml),
|
|
486
|
+
font_faces=font_faces,
|
|
487
|
+
char_properties=char_properties,
|
|
488
|
+
section_layouts=tuple(section_layouts),
|
|
489
|
+
table_summaries=tuple(table_summaries),
|
|
490
|
+
run_style_references=tuple(run_style_references),
|
|
491
|
+
proxy_metrics=collect_metrics(source_path),
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _write_part(package: HwpxPackage, part_name: str, destination: Path) -> Path:
|
|
496
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
497
|
+
destination.write_bytes(package.get_part(part_name))
|
|
498
|
+
return destination
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def extract_template_parts(
|
|
502
|
+
source: str | Path,
|
|
503
|
+
*,
|
|
504
|
+
extract_dir: str | Path | None = None,
|
|
505
|
+
extract_header: str | Path | None = None,
|
|
506
|
+
extract_section: str | Path | None = None,
|
|
507
|
+
extract_section_dir: str | Path | None = None,
|
|
508
|
+
) -> tuple[Path, ...]:
|
|
509
|
+
source_path = Path(source)
|
|
510
|
+
package = HwpxPackage.open(source_path)
|
|
511
|
+
written: list[Path] = []
|
|
512
|
+
|
|
513
|
+
if extract_dir is not None:
|
|
514
|
+
result = unpack_hwpx(source_path, extract_dir, pretty_xml=False)
|
|
515
|
+
written.extend(result.output_dir / entry.path for entry in result.entries)
|
|
516
|
+
written.append(result.metadata_path)
|
|
517
|
+
|
|
518
|
+
if extract_header is not None:
|
|
519
|
+
header_paths = package.header_paths()
|
|
520
|
+
if not header_paths:
|
|
521
|
+
raise FileNotFoundError("package does not contain a header part")
|
|
522
|
+
written.append(_write_part(package, header_paths[0], Path(extract_header)))
|
|
523
|
+
|
|
524
|
+
if extract_section is not None:
|
|
525
|
+
section_paths = package.section_paths()
|
|
526
|
+
if not section_paths:
|
|
527
|
+
raise FileNotFoundError("package does not contain a section part")
|
|
528
|
+
written.append(_write_part(package, section_paths[0], Path(extract_section)))
|
|
529
|
+
|
|
530
|
+
if extract_section_dir is not None:
|
|
531
|
+
section_root = Path(extract_section_dir)
|
|
532
|
+
section_root.mkdir(parents=True, exist_ok=True)
|
|
533
|
+
for part_name in package.section_paths():
|
|
534
|
+
written.append(_write_part(package, part_name, section_root / Path(part_name).name))
|
|
535
|
+
|
|
536
|
+
return tuple(written)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _print_summary(analysis: TemplateAnalysis) -> None:
|
|
540
|
+
metrics = analysis.proxy_metrics
|
|
541
|
+
print(f"source: {analysis.source_name}")
|
|
542
|
+
print(f"schema: {analysis.schema_version}")
|
|
543
|
+
print(f"manifest: {analysis.manifest_path}")
|
|
544
|
+
print(f"rootfiles: {', '.join(analysis.rootfiles) or '(none)'}")
|
|
545
|
+
print(f"headers: {', '.join(analysis.header_paths) or '(none)'}")
|
|
546
|
+
print(f"sections: {', '.join(analysis.section_paths) or '(none)'}")
|
|
547
|
+
print(f"masterPages: {', '.join(analysis.master_page_paths) or '(none)'}")
|
|
548
|
+
print(f"histories: {', '.join(analysis.history_paths) or '(none)'}")
|
|
549
|
+
print(f"BinData: {', '.join(analysis.bin_data_paths) or '(none)'}")
|
|
550
|
+
if analysis.version_path:
|
|
551
|
+
print(f"version part: {analysis.version_path}")
|
|
552
|
+
print(
|
|
553
|
+
"header styles: "
|
|
554
|
+
f"fonts={analysis.header_summary.font_count}, "
|
|
555
|
+
f"charPr={analysis.header_summary.char_pr_count}, "
|
|
556
|
+
f"paraPr={analysis.header_summary.para_pr_count}, "
|
|
557
|
+
f"borderFill={analysis.header_summary.border_fill_count}"
|
|
558
|
+
)
|
|
559
|
+
print(
|
|
560
|
+
"template enrichment: "
|
|
561
|
+
f"fontFaces={len(analysis.font_faces)}, "
|
|
562
|
+
f"charProperties={len(analysis.char_properties)}, "
|
|
563
|
+
f"tables={len(analysis.table_summaries)}, "
|
|
564
|
+
f"runStyleRefs={len(analysis.run_style_references)}"
|
|
565
|
+
)
|
|
566
|
+
print(
|
|
567
|
+
"layout-drift proxy: "
|
|
568
|
+
f"paragraphs={metrics.paragraph_count}, "
|
|
569
|
+
f"tables={metrics.table_count}, "
|
|
570
|
+
f"shapes={metrics.shape_count}, "
|
|
571
|
+
f"controls={metrics.control_count}, "
|
|
572
|
+
f"pageBreaks={metrics.page_break_count}, "
|
|
573
|
+
f"columnBreaks={metrics.column_break_count}"
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
578
|
+
parser = argparse.ArgumentParser(
|
|
579
|
+
description="Analyze a reference HWPX template for pack-ready, template-preserving workflows"
|
|
580
|
+
)
|
|
581
|
+
parser.add_argument("input", help="Input HWPX path")
|
|
582
|
+
parser.add_argument("--json", action="store_true", help="Print machine-readable JSON summary")
|
|
583
|
+
parser.add_argument("--output-json", help="Write the JSON summary to a file")
|
|
584
|
+
parser.add_argument("--schema-json", action="store_true", help="Print the agent-friendly JSON schema")
|
|
585
|
+
parser.add_argument("--output-schema-json", help="Write the agent-friendly JSON schema to a file")
|
|
586
|
+
parser.add_argument(
|
|
587
|
+
"--extract-dir",
|
|
588
|
+
help=(
|
|
589
|
+
"Create a pack-ready extracted workspace that preserves archive-relative paths "
|
|
590
|
+
"and hwpx-pack metadata"
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
parser.add_argument("--extract-header", help="Copy the first header.xml part to a path")
|
|
594
|
+
parser.add_argument("--extract-section", help="Copy the first section XML part to a path")
|
|
595
|
+
parser.add_argument(
|
|
596
|
+
"--extract-section-dir",
|
|
597
|
+
help="Backward-compatible alias that copies section*.xml files into a directory",
|
|
598
|
+
)
|
|
599
|
+
args = parser.parse_args(argv)
|
|
600
|
+
|
|
601
|
+
input_path = Path(args.input)
|
|
602
|
+
if not input_path.is_file():
|
|
603
|
+
print(f"ERROR: file not found: {input_path}")
|
|
604
|
+
return 1
|
|
605
|
+
|
|
606
|
+
try:
|
|
607
|
+
analysis = analyze_template(input_path)
|
|
608
|
+
schema_only = (
|
|
609
|
+
args.schema_json
|
|
610
|
+
and not args.json
|
|
611
|
+
and not args.output_json
|
|
612
|
+
and not args.extract_dir
|
|
613
|
+
and not args.extract_header
|
|
614
|
+
and not args.extract_section
|
|
615
|
+
and not args.extract_section_dir
|
|
616
|
+
)
|
|
617
|
+
if schema_only:
|
|
618
|
+
print(json.dumps(template_analysis_agent_schema(), ensure_ascii=False, indent=2))
|
|
619
|
+
return 0
|
|
620
|
+
written = extract_template_parts(
|
|
621
|
+
input_path,
|
|
622
|
+
extract_dir=args.extract_dir,
|
|
623
|
+
extract_header=args.extract_header,
|
|
624
|
+
extract_section=args.extract_section,
|
|
625
|
+
extract_section_dir=args.extract_section_dir,
|
|
626
|
+
)
|
|
627
|
+
except Exception as exc:
|
|
628
|
+
print(f"ERROR: {exc}")
|
|
629
|
+
return 1
|
|
630
|
+
|
|
631
|
+
if args.json or args.output_json:
|
|
632
|
+
payload = json.dumps(asdict(analysis), ensure_ascii=False, indent=2)
|
|
633
|
+
if args.json:
|
|
634
|
+
print(payload)
|
|
635
|
+
if args.output_json:
|
|
636
|
+
output_path = Path(args.output_json)
|
|
637
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
638
|
+
output_path.write_text(payload, encoding="utf-8")
|
|
639
|
+
else:
|
|
640
|
+
_print_summary(analysis)
|
|
641
|
+
|
|
642
|
+
if args.schema_json or args.output_schema_json:
|
|
643
|
+
schema_payload = json.dumps(template_analysis_agent_schema(), ensure_ascii=False, indent=2)
|
|
644
|
+
if args.schema_json:
|
|
645
|
+
print(schema_payload)
|
|
646
|
+
if args.output_schema_json:
|
|
647
|
+
schema_path = Path(args.output_schema_json)
|
|
648
|
+
schema_path.parent.mkdir(parents=True, exist_ok=True)
|
|
649
|
+
schema_path.write_text(schema_payload, encoding="utf-8")
|
|
650
|
+
|
|
651
|
+
for path in written:
|
|
652
|
+
print(f"extracted: {path}")
|
|
653
|
+
return 0
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
if __name__ == "__main__": # pragma: no cover - CLI convenience
|
|
657
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Canonical office-compliance policy owners."""
|
|
3
|
+
|
|
4
|
+
from .official_lint import (
|
|
5
|
+
OFFICIAL_DOCUMENT_STYLE_REPORT_VERSION,
|
|
6
|
+
inspect_official_document_style,
|
|
7
|
+
)
|
|
8
|
+
from .pii import (
|
|
9
|
+
DEFAULT_POLICY,
|
|
10
|
+
Confidence,
|
|
11
|
+
PiiLogFilter,
|
|
12
|
+
PIIPolicy,
|
|
13
|
+
PIISpan,
|
|
14
|
+
Pseudonymizer,
|
|
15
|
+
deidentify,
|
|
16
|
+
detect_pii,
|
|
17
|
+
mask_pii,
|
|
18
|
+
mask_value,
|
|
19
|
+
minimize_fields,
|
|
20
|
+
scrub_exception_message,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Confidence",
|
|
25
|
+
"DEFAULT_POLICY",
|
|
26
|
+
"OFFICIAL_DOCUMENT_STYLE_REPORT_VERSION",
|
|
27
|
+
"PIIPolicy",
|
|
28
|
+
"PIISpan",
|
|
29
|
+
"PiiLogFilter",
|
|
30
|
+
"Pseudonymizer",
|
|
31
|
+
"deidentify",
|
|
32
|
+
"detect_pii",
|
|
33
|
+
"inspect_official_document_style",
|
|
34
|
+
"mask_pii",
|
|
35
|
+
"mask_value",
|
|
36
|
+
"minimize_fields",
|
|
37
|
+
"scrub_exception_message",
|
|
38
|
+
]
|