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.
Files changed (217) hide show
  1. hwpx_automation/__init__.py +61 -0
  2. hwpx_automation/__init__.pyi +27 -0
  3. hwpx_automation/__main__.py +8 -0
  4. hwpx_automation/agent_document.py +392 -0
  5. hwpx_automation/api.py +136 -0
  6. hwpx_automation/blind_eval.py +407 -0
  7. hwpx_automation/capabilities.py +110 -0
  8. hwpx_automation/compat.py +48 -0
  9. hwpx_automation/configuration.py +60 -0
  10. hwpx_automation/core/__init__.py +2 -0
  11. hwpx_automation/core/content.py +762 -0
  12. hwpx_automation/core/context.py +111 -0
  13. hwpx_automation/core/diff.py +53 -0
  14. hwpx_automation/core/document.py +37 -0
  15. hwpx_automation/core/formatting.py +513 -0
  16. hwpx_automation/core/handles.py +24 -0
  17. hwpx_automation/core/locations.py +205 -0
  18. hwpx_automation/core/locator.py +162 -0
  19. hwpx_automation/core/plan.py +680 -0
  20. hwpx_automation/core/resources.py +42 -0
  21. hwpx_automation/core/search.py +296 -0
  22. hwpx_automation/core/transactions.py +434 -0
  23. hwpx_automation/core/txn.py +48 -0
  24. hwpx_automation/document_state.py +95 -0
  25. hwpx_automation/errors.py +174 -0
  26. hwpx_automation/execution_lock.py +15 -0
  27. hwpx_automation/fastmcp_adapter.py +672 -0
  28. hwpx_automation/form_fill.py +1177 -0
  29. hwpx_automation/form_output_models.py +223 -0
  30. hwpx_automation/handlers/__init__.py +2 -0
  31. hwpx_automation/handlers/_shared.py +377 -0
  32. hwpx_automation/handlers/agent_document.py +257 -0
  33. hwpx_automation/handlers/authoring.py +750 -0
  34. hwpx_automation/handlers/content_edit.py +1078 -0
  35. hwpx_automation/handlers/form_fill.py +607 -0
  36. hwpx_automation/handlers/layout_style.py +660 -0
  37. hwpx_automation/handlers/quality_render.py +566 -0
  38. hwpx_automation/handlers/read_export.py +1295 -0
  39. hwpx_automation/handlers/specialized.py +624 -0
  40. hwpx_automation/handlers/tracked_changes.py +589 -0
  41. hwpx_automation/handlers/workflow.py +105 -0
  42. hwpx_automation/hwp_converter.py +227 -0
  43. hwpx_automation/hwp_support.py +94 -0
  44. hwpx_automation/hwpx_ops.py +1439 -0
  45. hwpx_automation/identity.json +263 -0
  46. hwpx_automation/identity.py +18 -0
  47. hwpx_automation/ingest_adapters.py +85 -0
  48. hwpx_automation/markdown_plan.py +216 -0
  49. hwpx_automation/mcp_cli.py +29 -0
  50. hwpx_automation/metadata/tools_meta.py +40 -0
  51. hwpx_automation/mixed_form.py +3007 -0
  52. hwpx_automation/mutation_models.py +401 -0
  53. hwpx_automation/network_policy.py +232 -0
  54. hwpx_automation/office/__init__.py +14 -0
  55. hwpx_automation/office/agent/__init__.py +125 -0
  56. hwpx_automation/office/agent/_batch_verification.py +383 -0
  57. hwpx_automation/office/agent/blueprint/__init__.py +58 -0
  58. hwpx_automation/office/agent/blueprint/bundle.py +282 -0
  59. hwpx_automation/office/agent/blueprint/catalog.py +136 -0
  60. hwpx_automation/office/agent/blueprint/dump.py +520 -0
  61. hwpx_automation/office/agent/blueprint/mapping.py +312 -0
  62. hwpx_automation/office/agent/blueprint/model.py +722 -0
  63. hwpx_automation/office/agent/blueprint/native.py +621 -0
  64. hwpx_automation/office/agent/blueprint/replay.py +622 -0
  65. hwpx_automation/office/agent/catalog.py +252 -0
  66. hwpx_automation/office/agent/cli.py +647 -0
  67. hwpx_automation/office/agent/commands.py +1383 -0
  68. hwpx_automation/office/agent/document.py +801 -0
  69. hwpx_automation/office/agent/form_plan.py +1760 -0
  70. hwpx_automation/office/agent/model.py +808 -0
  71. hwpx_automation/office/agent/path.py +155 -0
  72. hwpx_automation/office/agent/query.py +230 -0
  73. hwpx_automation/office/agent/story.py +207 -0
  74. hwpx_automation/office/authoring/__init__.py +3542 -0
  75. hwpx_automation/office/authoring/advanced_generators.py +154 -0
  76. hwpx_automation/office/authoring/builder/__init__.py +52 -0
  77. hwpx_automation/office/authoring/builder/core.py +996 -0
  78. hwpx_automation/office/authoring/builder/report.py +195 -0
  79. hwpx_automation/office/authoring/design/__init__.py +30 -0
  80. hwpx_automation/office/authoring/design/_support.py +144 -0
  81. hwpx_automation/office/authoring/design/composer.py +282 -0
  82. hwpx_automation/office/authoring/design/harvest.py +305 -0
  83. hwpx_automation/office/authoring/design/plan.py +69 -0
  84. hwpx_automation/office/authoring/design/profile.py +88 -0
  85. hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
  86. hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
  87. hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
  88. hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
  89. hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
  90. hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  91. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
  92. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
  93. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
  94. hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
  95. hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  96. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
  97. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
  98. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
  99. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
  100. hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
  101. hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  102. hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
  103. hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
  104. hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
  105. hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
  106. hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
  107. hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  108. hwpx_automation/office/authoring/design/validator.py +107 -0
  109. hwpx_automation/office/authoring/presets/__init__.py +22 -0
  110. hwpx_automation/office/authoring/presets/proposal.py +538 -0
  111. hwpx_automation/office/authoring/report_parser.py +141 -0
  112. hwpx_automation/office/authoring/style_profile.py +437 -0
  113. hwpx_automation/office/authoring/template_analyzer.py +657 -0
  114. hwpx_automation/office/compliance/__init__.py +38 -0
  115. hwpx_automation/office/compliance/official_lint.py +478 -0
  116. hwpx_automation/office/compliance/pii.py +388 -0
  117. hwpx_automation/office/document_ops/__init__.py +13 -0
  118. hwpx_automation/office/document_ops/comparison.py +62 -0
  119. hwpx_automation/office/document_ops/mail_merge.py +73 -0
  120. hwpx_automation/office/document_ops/redline.py +35 -0
  121. hwpx_automation/office/evalplan/__init__.py +36 -0
  122. hwpx_automation/office/evalplan/runtime.py +2762 -0
  123. hwpx_automation/office/exam/__init__.py +44 -0
  124. hwpx_automation/office/exam/compose.py +282 -0
  125. hwpx_automation/office/exam/ir.py +44 -0
  126. hwpx_automation/office/exam/measure.py +163 -0
  127. hwpx_automation/office/exam/parser.py +150 -0
  128. hwpx_automation/office/exam/profile.py +123 -0
  129. hwpx_automation/office/form_fill/__init__.py +66 -0
  130. hwpx_automation/office/form_fill/classification.py +108 -0
  131. hwpx_automation/office/form_fill/fill_residue.py +242 -0
  132. hwpx_automation/office/form_fill/fit/__init__.py +36 -0
  133. hwpx_automation/office/form_fill/fit/apply.py +24 -0
  134. hwpx_automation/office/form_fill/fit/engine.py +24 -0
  135. hwpx_automation/office/form_fill/fit/measure.py +50 -0
  136. hwpx_automation/office/form_fill/fit/policy.py +28 -0
  137. hwpx_automation/office/form_fill/fit/report.py +28 -0
  138. hwpx_automation/office/form_fill/fit/seal.py +457 -0
  139. hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
  140. hwpx_automation/office/form_fill/guidance.py +704 -0
  141. hwpx_automation/office/form_fill/quality.py +961 -0
  142. hwpx_automation/office/form_fill/split_run.py +333 -0
  143. hwpx_automation/office/form_fill/template_formfit.py +656 -0
  144. hwpx_automation/office/house_style/__init__.py +196 -0
  145. hwpx_automation/office/house_style/composition.py +68 -0
  146. hwpx_automation/office/house_style/data/bank.json +625 -0
  147. hwpx_automation/office/house_style/data/genres.json +43 -0
  148. hwpx_automation/office/quality/__init__.py +14 -0
  149. hwpx_automation/office/quality/page_guard.py +277 -0
  150. hwpx_automation/office/rendering/__init__.py +145 -0
  151. hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
  152. hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
  153. hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
  154. hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
  155. hwpx_automation/office/rendering/block_splits.py +76 -0
  156. hwpx_automation/office/rendering/detectors.py +151 -0
  157. hwpx_automation/office/rendering/diff.py +153 -0
  158. hwpx_automation/office/rendering/fixture_corpus.py +215 -0
  159. hwpx_automation/office/rendering/oracle.py +909 -0
  160. hwpx_automation/office/rendering/page_qa.py +245 -0
  161. hwpx_automation/office/rendering/qa_contracts.py +293 -0
  162. hwpx_automation/office/rendering/qa_metrics.py +241 -0
  163. hwpx_automation/office/rendering/worker.py +290 -0
  164. hwpx_automation/office/utilities/__init__.py +12 -0
  165. hwpx_automation/office/utilities/table_compute.py +477 -0
  166. hwpx_automation/ops_services/__init__.py +1 -0
  167. hwpx_automation/ops_services/_border_fill.py +283 -0
  168. hwpx_automation/ops_services/composition.py +55 -0
  169. hwpx_automation/ops_services/content_layout.py +322 -0
  170. hwpx_automation/ops_services/context.py +213 -0
  171. hwpx_automation/ops_services/form_fields.py +557 -0
  172. hwpx_automation/ops_services/media.py +178 -0
  173. hwpx_automation/ops_services/memo_style.py +477 -0
  174. hwpx_automation/ops_services/package_validation.py +166 -0
  175. hwpx_automation/ops_services/planning.py +201 -0
  176. hwpx_automation/ops_services/preview_export.py +585 -0
  177. hwpx_automation/ops_services/read_query.py +601 -0
  178. hwpx_automation/ops_services/save_policy.py +604 -0
  179. hwpx_automation/ops_services/tables.py +539 -0
  180. hwpx_automation/ops_services/transactions.py +616 -0
  181. hwpx_automation/preview_output_models.py +69 -0
  182. hwpx_automation/public-modules.json +206 -0
  183. hwpx_automation/py.typed +1 -0
  184. hwpx_automation/quality.py +351 -0
  185. hwpx_automation/quality_generation.py +725 -0
  186. hwpx_automation/runtime.py +321 -0
  187. hwpx_automation/runtime_services.py +100 -0
  188. hwpx_automation/server.py +259 -0
  189. hwpx_automation/storage.py +747 -0
  190. hwpx_automation/tool_bindings.py +170 -0
  191. hwpx_automation/tool_contract.py +982 -0
  192. hwpx_automation/upstream.py +755 -0
  193. hwpx_automation/utils/__init__.py +2 -0
  194. hwpx_automation/utils/helpers.py +29 -0
  195. hwpx_automation/visual_qa.py +667 -0
  196. hwpx_automation/workflow/__init__.py +55 -0
  197. hwpx_automation/workflow/adapters.py +482 -0
  198. hwpx_automation/workflow/dispatcher.py +213 -0
  199. hwpx_automation/workflow/models.py +243 -0
  200. hwpx_automation/workflow/policy.py +198 -0
  201. hwpx_automation/workflow/render_contracts.py +173 -0
  202. hwpx_automation/workflow/render_metrics.py +196 -0
  203. hwpx_automation/workflow/render_queue.py +482 -0
  204. hwpx_automation/workflow/render_security.py +172 -0
  205. hwpx_automation/workflow/render_transport.py +369 -0
  206. hwpx_automation/workflow/rendering.py +206 -0
  207. hwpx_automation/workflow/service.py +758 -0
  208. hwpx_automation/workflow/state_machine.py +65 -0
  209. hwpx_automation/workflow/store.py +747 -0
  210. hwpx_automation/workspace.py +1694 -0
  211. python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
  212. python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
  213. python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
  214. python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
  215. python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
  216. python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
  217. python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,305 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Harvest a profile from a real Hancom-saved document (plan §2 Phase E, task 1).
3
+
4
+ Given a genuine ``.hwpx`` it writes, under ``profiles/<id>/``:
5
+
6
+ * ``template.hwpx`` — a **body-stripped skeleton**: the real document with its
7
+ section body reduced to a single ``secPr``-carrying paragraph. ``header.xml``
8
+ (every style/font/borderFill) and the page setup survive untouched, so the
9
+ committed artifact is small and carries no document content (no PII), yet is
10
+ genuinely Hancom-derived — never imagined XML.
11
+ * ``fragments/{title,heading,body,info_table}.xml`` — real ``<hp:p>`` / ``<hp:tbl>``
12
+ elements harvested from the source, with their text **sanitised to placeholders**
13
+ (the style is kept, the content dropped).
14
+ * ``profile.json`` — the manifest the loader reads.
15
+
16
+ This is an authoring-time tool (run once per profile); the committed artifacts are
17
+ what production uses.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from collections import Counter
23
+ from copy import deepcopy
24
+ from pathlib import Path
25
+
26
+ from lxml import etree # pyright: ignore[reportAttributeAccessIssue]
27
+
28
+ from hwpx.document import HwpxDocument
29
+
30
+ import hwpx_automation.office.authoring.design._support as S
31
+
32
+ PROFILE_SCHEMA = "hwpx.design.profile.v1"
33
+
34
+ _PLACEHOLDER = {
35
+ "title": "{{title}}",
36
+ "heading": "{{heading}}",
37
+ "body": "{{body}}",
38
+ }
39
+
40
+
41
+ def _font_height(doc, ref) -> int:
42
+ style = doc.char_property(ref)
43
+ if style is None:
44
+ return 0
45
+ try:
46
+ return int(style.attributes.get("height") or 0)
47
+ except ValueError:
48
+ return 0
49
+
50
+
51
+ def _text_paragraphs(doc):
52
+ out = []
53
+ for index, para in enumerate(doc.sections[0].paragraphs):
54
+ if getattr(para, "tables", []):
55
+ continue
56
+ text = (para.text or "").strip()
57
+ if not text:
58
+ continue
59
+ runs = para.runs
60
+ cref = runs[0].char_pr_id_ref if runs else "0"
61
+ out.append(
62
+ {
63
+ "index": index,
64
+ "para": para.element.get("paraPrIDRef"),
65
+ "char": cref,
66
+ "h": _font_height(doc, cref),
67
+ "len": len(text),
68
+ "element": para.element,
69
+ }
70
+ )
71
+ return out
72
+
73
+
74
+ def _pick_fragments(doc) -> dict[str, etree._Element]:
75
+ tps = _text_paragraphs(doc)
76
+ if not tps:
77
+ raise ValueError("no text paragraphs to harvest")
78
+ prose = [t for t in tps if t["len"] >= 8] or tps
79
+
80
+ title = max(tps, key=lambda t: t["h"])
81
+ combo = Counter((t["para"], t["char"]) for t in prose)
82
+ body_key = combo.most_common(1)[0][0]
83
+ body = next(t for t in prose if (t["para"], t["char"]) == body_key)
84
+ head_pool = [
85
+ t for t in tps
86
+ if (t["para"], t["char"]) != body_key
87
+ and Counter((x["para"], x["char"]) for x in tps)[(t["para"], t["char"])] >= 2
88
+ and t["h"] >= body["h"]
89
+ ]
90
+ heading = max(head_pool, key=lambda t: t["h"], default=title)
91
+
92
+ picks = {"title": title, "heading": heading, "body": body}
93
+ fragments = {role: deepcopy(p["element"]) for role, p in picks.items()}
94
+ for role, frag in fragments.items():
95
+ S.set_paragraph_text(frag, _PLACEHOLDER[role])
96
+
97
+ info = _pick_info_table(doc)
98
+ if info is not None:
99
+ fragments["info_table"] = info
100
+ return fragments
101
+
102
+
103
+ def _references_binary(element: etree._Element) -> bool:
104
+ """True if *element* embeds an image / binary (so harvesting would dangle)."""
105
+
106
+ for node in element.iter():
107
+ if S.ln(node.tag).lower() in ("pic", "img", "image", "ole", "drawing"):
108
+ return True
109
+ for key in node.attrib:
110
+ if "binaryitemidref" in S.ln(key).lower() or "bindata" in S.ln(key).lower():
111
+ return True
112
+ return False
113
+
114
+
115
+ def _is_simple_grid(table_element: etree._Element) -> bool:
116
+ """True if every cell is 1×1 (no row/col merges) — safe to row-clone."""
117
+
118
+ for tc in S.iter_local(table_element, "tc"):
119
+ span = next((c for c in tc if S.ln(c.tag) == "cellSpan"), None)
120
+ if span is not None:
121
+ if int(span.get("rowSpan", "1")) > 1 or int(span.get("colSpan", "1")) > 1:
122
+ return False
123
+ return True
124
+
125
+
126
+ def _pick_info_table(doc) -> etree._Element | None:
127
+ best = None
128
+ for para in doc.sections[0].paragraphs:
129
+ for table in getattr(para, "tables", []):
130
+ rows, cols = table.row_count, table.column_count
131
+ if not (2 <= cols <= 4 and 2 <= rows <= 12):
132
+ continue
133
+ if _references_binary(table.element):
134
+ continue # text-only key-value tables, never image grids
135
+ if not _is_simple_grid(table.element):
136
+ continue # merge-free grids only — the composer row-clones them
137
+ score = rows * cols
138
+ if best is None or score < best[0]:
139
+ best = (score, table)
140
+ if best is None:
141
+ return None
142
+ # Harvest the *paragraph* that wraps the table — a table is not a valid
143
+ # section child on its own; it must ride inside an <hp:p>.
144
+ frag = deepcopy(best[1].paragraph.element)
145
+ # Sanitise every cell to a neutral placeholder; structure is what we keep.
146
+ for tc in S.iter_local(frag, "tc"):
147
+ sublist = _subList(tc)
148
+ for para in S.children_local(sublist if sublist is not None else tc, "p"):
149
+ S.set_paragraph_text(para, "{{cell}}")
150
+ S.strip_lineseg(frag)
151
+ return frag
152
+
153
+
154
+ def _subList(tc: etree._Element):
155
+ for child in tc:
156
+ if S.ln(child.tag) == "subList":
157
+ return child
158
+ return None
159
+
160
+
161
+ def _build_skeleton(source: Path, out_path: Path) -> dict:
162
+ doc = HwpxDocument.open(source)
163
+ section = doc.sections[0]
164
+ secpr = S.find_secpr(section.element)
165
+ if secpr is None:
166
+ doc.close()
167
+ raise ValueError("template has no secPr (page setup)")
168
+ secpr = deepcopy(secpr)
169
+
170
+ empty = etree.SubElement(section.element, S.hp(section.element, "p"))
171
+ run = etree.SubElement(empty, S.hp(section.element, "run"), {"charPrIDRef": "0"})
172
+ etree.SubElement(run, S.hp(section.element, "t")).text = ""
173
+ section.element.remove(empty) # detach; replace_section_body re-inserts
174
+ S.replace_section_body(section.element, [empty])
175
+ S.move_secpr_into(empty, secpr)
176
+ section.mark_dirty()
177
+
178
+ stripped = _strip_binaries(doc)
179
+ _scrub_metadata(doc)
180
+
181
+ page = {}
182
+ try:
183
+ ps, pm = section.properties.page_size, section.properties.page_margins
184
+ page = {
185
+ "width": ps.width, "height": ps.height, "orientation": ps.orientation,
186
+ "margins": {"left": pm.left, "right": pm.right, "top": pm.top, "bottom": pm.bottom},
187
+ }
188
+ except Exception: # pragma: no cover - diagnostics only
189
+ page = {}
190
+
191
+ report = doc.save_report(out_path)
192
+ style_count = len(doc.char_properties)
193
+ doc.close()
194
+ return {
195
+ "page": page,
196
+ "char_pr_count": style_count,
197
+ "open_safe": report.open_safety.ok,
198
+ "stripped_parts": stripped,
199
+ }
200
+
201
+
202
+ def _strip_binaries(doc) -> list[str]:
203
+ """Drop embedded images + the source preview (size + PII) from the skeleton.
204
+
205
+ The body is already reduced to one empty paragraph, so nothing references
206
+ these parts; removing them (and their manifest items) keeps the committed
207
+ skeleton small and content-free while leaving every style in header.xml.
208
+ """
209
+
210
+ package = doc._package
211
+ removed: list[str] = []
212
+
213
+ for item in list(package._manifest_items()):
214
+ href = item.get("href", "") or ""
215
+ media = item.get("media-type", "") or ""
216
+ base = href.rsplit("/", 1)[-1]
217
+ if (
218
+ "BinData/" in href
219
+ or base.startswith("PrvImage")
220
+ or media.lower().startswith("image/")
221
+ ):
222
+ item_id = item.get("id")
223
+ if item_id:
224
+ package.remove_manifest_item(item_id)
225
+
226
+ for name in list(package.part_names()):
227
+ if name.startswith("BinData/") or name.startswith("Preview/PrvImage"):
228
+ try:
229
+ package.delete(name)
230
+ removed.append(name)
231
+ except Exception: # pragma: no cover - defensive
232
+ pass
233
+
234
+ if package.has_part("Preview/PrvText.txt"):
235
+ package.set_part("Preview/PrvText.txt", b"") # clear source text (PII)
236
+ return removed
237
+
238
+
239
+ # Manifest (content.hpf) metadata that carries source PII — author, the person who
240
+ # last saved, the source title, and timestamps. Cleared so generated docs inherit
241
+ # nothing identifying.
242
+ _SCRUB_META_NAMES = {
243
+ "creator", "lastsaveby", "subject", "description", "keyword",
244
+ "date", "CreatedDate", "ModifiedDate",
245
+ }
246
+
247
+
248
+ def _scrub_metadata(doc) -> None:
249
+ """Blank source-identifying OPF metadata in Contents/content.hpf (PII)."""
250
+
251
+ package = doc._package
252
+ try:
253
+ tree = package.manifest_tree()
254
+ except Exception: # pragma: no cover - defensive
255
+ return
256
+ for element in tree.iter():
257
+ name = S.ln(element.tag)
258
+ if name == "title":
259
+ element.text = ""
260
+ elif name == "meta" and element.get("name") in _SCRUB_META_NAMES:
261
+ element.text = ""
262
+ package.set_part(package.main_content.full_path, tree)
263
+
264
+
265
+ def harvest_profile(source: str | Path, profile_id: str, out_root: str | Path) -> dict:
266
+ """Harvest *source* into ``<out_root>/<profile_id>/`` and return the manifest."""
267
+
268
+ source = Path(source)
269
+ out_dir = Path(out_root) / profile_id
270
+ (out_dir / "fragments").mkdir(parents=True, exist_ok=True)
271
+
272
+ # Fragments first (reads the full source), then the skeleton (rewrites body).
273
+ doc = HwpxDocument.open(source)
274
+ fragments = _pick_fragments(doc)
275
+ doc.close()
276
+
277
+ roles = []
278
+ for role, frag in fragments.items():
279
+ path = out_dir / "fragments" / f"{role}.xml"
280
+ path.write_bytes(etree.tostring(frag, encoding="UTF-8"))
281
+ roles.append(role)
282
+
283
+ skel = _build_skeleton(source, out_dir / "template.hwpx")
284
+
285
+ manifest = {
286
+ "schemaVersion": PROFILE_SCHEMA,
287
+ "id": profile_id,
288
+ "template": "template.hwpx",
289
+ "fragments": {role: f"fragments/{role}.xml" for role in roles},
290
+ "page": skel["page"],
291
+ "char_pr_count": skel["char_pr_count"],
292
+ "style_coverage_threshold": 0.98,
293
+ "source_basename": source.name,
294
+ }
295
+ (out_dir / "profile.json").write_text(
296
+ json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8"
297
+ )
298
+ return {
299
+ **manifest,
300
+ "skeleton_open_safe": skel["open_safe"],
301
+ "stripped_parts": skel.get("stripped_parts", []),
302
+ }
303
+
304
+
305
+ __all__ = ["harvest_profile", "PROFILE_SCHEMA"]
@@ -0,0 +1,69 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """``DocumentPlan`` — the model-facing description of a new document (Appendix B).
3
+
4
+ The LLM emits a plan (``hwpx.document_plan.v1``), never XML. Phase E lowers it
5
+ natively onto a verified Hancom-saved template + harvested fragments
6
+ (:mod:`hwpx.design.composer`).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Literal
12
+
13
+ DOCUMENT_PLAN_SCHEMA = "hwpx.document_plan.v1"
14
+
15
+ BlockType = Literal["paragraph", "table"]
16
+ # Paragraph roles map to harvested fragments; table → the info-table fragment.
17
+ BlockRole = Literal["title", "heading", "subheading", "body", "info"]
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class Block:
22
+ """One block of a :class:`DocumentPlan`."""
23
+
24
+ type: BlockType
25
+ role: BlockRole = "body"
26
+ text: str = ""
27
+ columns: list[str] = field(default_factory=list)
28
+ rows: list[list[str]] = field(default_factory=list)
29
+
30
+ @classmethod
31
+ def from_dict(cls, data: dict[str, Any]) -> "Block":
32
+ return cls(
33
+ type=data.get("type", "paragraph"),
34
+ role=data.get("role", "body"),
35
+ text=str(data.get("text", "")),
36
+ columns=[str(c) for c in data.get("columns", [])],
37
+ rows=[[str(c) for c in row] for row in data.get("rows", [])],
38
+ )
39
+
40
+
41
+ @dataclass(slots=True)
42
+ class DocumentPlan:
43
+ """A new-document plan (Appendix B ``hwpx.document_plan.v1``)."""
44
+
45
+ profile: str
46
+ title: str = ""
47
+ blocks: list[Block] = field(default_factory=list)
48
+
49
+ @classmethod
50
+ def from_dict(cls, data: dict[str, Any]) -> "DocumentPlan":
51
+ if not data.get("profile"):
52
+ raise ValueError("DocumentPlan requires a 'profile'")
53
+ return cls(
54
+ profile=str(data["profile"]),
55
+ title=str(data.get("title", "")),
56
+ blocks=[Block.from_dict(b) for b in data.get("blocks", [])],
57
+ )
58
+
59
+ def iter_blocks(self) -> list[Block]:
60
+ """Blocks with the title promoted to a leading title block."""
61
+
62
+ blocks: list[Block] = []
63
+ if self.title:
64
+ blocks.append(Block(type="paragraph", role="title", text=self.title))
65
+ blocks.extend(self.blocks)
66
+ return blocks
67
+
68
+
69
+ __all__ = ["DocumentPlan", "Block", "BlockType", "BlockRole", "DOCUMENT_PLAN_SCHEMA"]
@@ -0,0 +1,88 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Profile loader + registry (plan §2 Phase E, task 1).
3
+
4
+ A :class:`Profile` bundles a verified Hancom-saved skeleton ``template.hwpx`` with
5
+ the harvested ``<hp:p>``/``<hp:tbl>`` fragments keyed by role. The default registry
6
+ loads the profiles committed under ``hwpx/design/profiles/``.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass, field
12
+ from functools import lru_cache
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from lxml import etree # pyright: ignore[reportAttributeAccessIssue]
17
+
18
+ PROFILES_DIRNAME = "profiles"
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class Profile:
23
+ """A verified template skeleton + its harvested fragments."""
24
+
25
+ id: str
26
+ root: Path
27
+ manifest: dict[str, Any]
28
+ template_bytes: bytes
29
+ _fragments: dict[str, bytes] = field(default_factory=dict)
30
+
31
+ @property
32
+ def style_coverage_threshold(self) -> float:
33
+ return float(self.manifest.get("style_coverage_threshold", 0.98))
34
+
35
+ @property
36
+ def roles(self) -> list[str]:
37
+ return list(self._fragments)
38
+
39
+ def has_role(self, role: str) -> bool:
40
+ return role in self._fragments
41
+
42
+ def fragment(self, role: str) -> etree._Element:
43
+ """Return a fresh (deep, detached) clone of the *role* fragment element."""
44
+
45
+ if role not in self._fragments:
46
+ raise KeyError(f"profile {self.id!r} has no fragment for role {role!r}")
47
+ return etree.fromstring(self._fragments[role])
48
+
49
+ @classmethod
50
+ def load(cls, root: str | Path) -> "Profile":
51
+ root = Path(root)
52
+ manifest = json.loads((root / "profile.json").read_text(encoding="utf-8"))
53
+ template_bytes = (root / manifest["template"]).read_bytes()
54
+ fragments: dict[str, bytes] = {}
55
+ for role, rel in manifest.get("fragments", {}).items():
56
+ fragments[role] = (root / rel).read_bytes()
57
+ return cls(
58
+ id=manifest["id"],
59
+ root=root,
60
+ manifest=manifest,
61
+ template_bytes=template_bytes,
62
+ _fragments=fragments,
63
+ )
64
+
65
+
66
+ @lru_cache(maxsize=1)
67
+ def _registry_root() -> Path:
68
+ return Path(__file__).resolve().parent / PROFILES_DIRNAME
69
+
70
+
71
+ def available_profiles() -> list[str]:
72
+ root = _registry_root()
73
+ return sorted(p.name for p in root.iterdir() if (p / "profile.json").exists())
74
+
75
+
76
+ @lru_cache(maxsize=None)
77
+ def load_profile(profile_id: str) -> Profile:
78
+ """Load a committed profile by id (cached)."""
79
+
80
+ root = _registry_root() / profile_id
81
+ if not (root / "profile.json").exists():
82
+ raise KeyError(
83
+ f"unknown profile {profile_id!r}; available: {available_profiles()}"
84
+ )
85
+ return Profile.load(root)
86
+
87
+
88
+ __all__ = ["Profile", "load_profile", "available_profiles", "PROFILES_DIRNAME"]
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="107" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="142"><hp:t>{{body}}</hp:t></hp:run><hp:run charPrIDRef="32"><hp:t></hp:t></hp:run><hp:run charPrIDRef="143"><hp:t></hp:t></hp:run><hp:run charPrIDRef="160"><hp:t></hp:t></hp:run><hp:run charPrIDRef="174"><hp:t></hp:t></hp:run><hp:run charPrIDRef="163"><hp:t></hp:t></hp:run><hp:run charPrIDRef="145"><hp:t></hp:t></hp:run><hp:run charPrIDRef="146"><hp:t></hp:t></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="0" paraPrIDRef="1" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="141"><hp:t>{{heading}}</hp:t></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="0" paraPrIDRef="93" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="161"><hp:t> </hp:t><hp:tbl id="2052787869" zOrder="32" numberingType="TABLE" textWrap="TOP_AND_BOTTOM" textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" pageBreak="CELL" repeatHeader="1" rowCnt="3" colCnt="2" cellSpacing="0" borderFillIDRef="4" noAdjust="0"><hp:sz width="46946" widthRelTo="ABSOLUTE" height="19347" heightRelTo="ABSOLUTE" protect="0"/><hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/><hp:outMargin left="0" right="0" top="0" bottom="0"/><hp:inMargin left="510" right="510" top="141" bottom="141"/><hp:tr><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="238"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="2147483648" paraPrIDRef="66" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="79"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="0" rowAddr="0"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="5253" height="1639"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="238"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="2147483648" paraPrIDRef="6" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="79"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="1" rowAddr="0"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="41693" height="1639"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc></hp:tr><hp:tr><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="258"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="2147483648" paraPrIDRef="66" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="69"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="0" rowAddr="1"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="5253" height="8854"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="258"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="2147483648" paraPrIDRef="103" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="80"><hp:t>{{cell}}</hp:t></hp:run></hp:p><hp:p id="0" paraPrIDRef="103" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="80"><hp:t>{{cell}}</hp:t></hp:run></hp:p><hp:p id="0" paraPrIDRef="103" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="80"><hp:t>{{cell}}</hp:t></hp:run></hp:p><hp:p id="0" paraPrIDRef="103" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="80"><hp:t>{{cell}}</hp:t></hp:run></hp:p><hp:p id="0" paraPrIDRef="103" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="80"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="1" rowAddr="1"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="41693" height="8854"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc></hp:tr><hp:tr><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="259"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="0" paraPrIDRef="66" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="69"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="0" rowAddr="2"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="5253" height="8854"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="259"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="0" paraPrIDRef="103" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="80"><hp:t>{{cell}}</hp:t></hp:run></hp:p><hp:p id="0" paraPrIDRef="103" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="80"><hp:t>{{cell}}</hp:t></hp:run></hp:p><hp:p id="0" paraPrIDRef="103" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="80"><hp:t>{{cell}}</hp:t></hp:run></hp:p><hp:p id="0" paraPrIDRef="103" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="80"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="1" rowAddr="2"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="41693" height="8854"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc></hp:tr></hp:tbl><hp:t/></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="100" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="141"><hp:t>{{title}}</hp:t></hp:run></hp:p>
@@ -0,0 +1,25 @@
1
+ {
2
+ "schemaVersion": "hwpx.design.profile.v1",
3
+ "id": "application_form",
4
+ "template": "template.hwpx",
5
+ "fragments": {
6
+ "title": "fragments/title.xml",
7
+ "heading": "fragments/heading.xml",
8
+ "body": "fragments/body.xml",
9
+ "info_table": "fragments/info_table.xml"
10
+ },
11
+ "page": {
12
+ "width": 59528,
13
+ "height": 84188,
14
+ "orientation": "WIDELY",
15
+ "margins": {
16
+ "left": 5669,
17
+ "right": 5669,
18
+ "top": 4251,
19
+ "bottom": 4251
20
+ }
21
+ },
22
+ "char_pr_count": 249,
23
+ "style_coverage_threshold": 0.98,
24
+ "source_basename": "2026%EB%85%84+AI+%EC%A4%91%EC%A0%90%ED%95%99%EA%B5%90+%EC%8B%A0%EC%B2%AD%EC%84%9C+%EB%B0%8F+%EC%9A%B4%EC%98%81%EA%B3%84%ED%9A%8D%EC%84%9C%28%EC%9C%A0%ED%98%951_%EB%A7%A4%EC%9B%90%EA%B3%A0%29_%EC%97%90%EB%93%80%EB%84%B7+%ED%83%91%EC%9E%AC%EC%9A%A9.hwpx"
25
+ }
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="1" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="26"><hp:t>{{body}}</hp:t></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="1" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="25"><hp:t>{{heading}}</hp:t></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="25" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="17"><hp:t>{{title}}</hp:t></hp:run></hp:p>
@@ -0,0 +1,24 @@
1
+ {
2
+ "schemaVersion": "hwpx.design.profile.v1",
3
+ "id": "home_notice",
4
+ "template": "template.hwpx",
5
+ "fragments": {
6
+ "title": "fragments/title.xml",
7
+ "heading": "fragments/heading.xml",
8
+ "body": "fragments/body.xml"
9
+ },
10
+ "page": {
11
+ "width": 59528,
12
+ "height": 84186,
13
+ "orientation": "WIDELY",
14
+ "margins": {
15
+ "left": 4251,
16
+ "right": 4251,
17
+ "top": 1417,
18
+ "bottom": 0
19
+ }
20
+ },
21
+ "char_pr_count": 54,
22
+ "style_coverage_threshold": 0.98,
23
+ "source_basename": "ganghwa_records.hwpx"
24
+ }
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="33" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="18"><hp:t>{{body}}</hp:t></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="0" paraPrIDRef="25" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="13"><hp:t>{{heading}}</hp:t></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="0" paraPrIDRef="28" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="15"><hp:t> </hp:t></hp:run><hp:run charPrIDRef="14"><hp:tbl id="1465524190" zOrder="10" numberingType="TABLE" textWrap="TOP_AND_BOTTOM" textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" pageBreak="CELL" repeatHeader="1" rowCnt="2" colCnt="3" cellSpacing="0" borderFillIDRef="12" noAdjust="0"><hp:sz width="46490" widthRelTo="ABSOLUTE" height="7274" heightRelTo="ABSOLUTE" protect="0"/><hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/><hp:outMargin left="141" right="141" top="141" bottom="141"/><hp:inMargin left="510" right="510" top="141" bottom="141"/><hp:tr><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="15"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="0" paraPrIDRef="22" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="42"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="0" rowAddr="0"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="13610" height="2414"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="15"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="0" paraPrIDRef="22" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="42"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="1" rowAddr="0"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="13610" height="2414"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="15"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="0" paraPrIDRef="22" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="52"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="2" rowAddr="0"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="19270" height="2414"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc></hp:tr><hp:tr><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="12"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="0" paraPrIDRef="58" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="42"><hp:t>{{cell}}</hp:t></hp:run></hp:p><hp:p id="0" paraPrIDRef="58" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="42"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="0" rowAddr="1"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="13610" height="4860"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="12"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="0" paraPrIDRef="58" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="42"><hp:t>{{cell}}</hp:t></hp:run></hp:p><hp:p id="0" paraPrIDRef="58" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="42"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="1" rowAddr="1"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="13610" height="4860"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="12"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="0" paraPrIDRef="58" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="52"><hp:t>{{cell}}</hp:t></hp:run></hp:p><hp:p id="0" paraPrIDRef="58" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="52"><hp:t>{{cell}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="2" rowAddr="1"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="19270" height="4860"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc></hp:tr></hp:tbl><hp:t/></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="0" paraPrIDRef="25" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="13"><hp:t>{{title}}</hp:t></hp:run></hp:p>
@@ -0,0 +1,25 @@
1
+ {
2
+ "schemaVersion": "hwpx.design.profile.v1",
3
+ "id": "official_notice",
4
+ "template": "template.hwpx",
5
+ "fragments": {
6
+ "title": "fragments/title.xml",
7
+ "heading": "fragments/heading.xml",
8
+ "body": "fragments/body.xml",
9
+ "info_table": "fragments/info_table.xml"
10
+ },
11
+ "page": {
12
+ "width": 59528,
13
+ "height": 84188,
14
+ "orientation": "WIDELY",
15
+ "margins": {
16
+ "left": 5669,
17
+ "right": 5669,
18
+ "top": 4251,
19
+ "bottom": 2834
20
+ }
21
+ },
22
+ "char_pr_count": 65,
23
+ "style_coverage_threshold": 0.98,
24
+ "source_basename": "synthetic_official_notice_source.hwpx"
25
+ }
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="0" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="52"><hp:t>{{body}}</hp:t></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="0" paraPrIDRef="22" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="26"><hp:t>{{heading}}</hp:t></hp:run><hp:run charPrIDRef="26"/></hp:p>