@kimdayoun/hwpx-mcp 0.3.3 → 0.3.5

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.
@@ -31,6 +31,19 @@ export declare class HwpxParser {
31
31
  private static parseColumnDef;
32
32
  private static parseImageEffects;
33
33
  private static parseParagraphsSimple;
34
+ /**
35
+ * The paragraph with every nested container (table, text box, drawing
36
+ * object, header/footer, caption, equation, note…) cut out, leaving only its
37
+ * own runs and text. Section paragraphs are read from this: the paragraphs
38
+ * inside those containers are listed as elements of their own, and the save
39
+ * path writes a paragraph's text into exactly these own <hp:t>
40
+ * (HwpxDocument.ownRunText). Reading the whole paragraph instead ended its
41
+ * run at the first nested </hp:run>: a paragraph "[text box] own text" read
42
+ * as the text box's text, its own text was never seen, and a whole-paragraph
43
+ * edit reopened as the old text box text (CodeRabbit, PR #17; 41 of 16,733
44
+ * paragraphs with own text in 275 Hancom originals lost it on reading).
45
+ */
46
+ private static withoutNestedContent;
34
47
  private static parseParagraph;
35
48
  private static parseRun;
36
49
  private static processTextContent;
@@ -1283,7 +1283,9 @@ class HwpxParser {
1283
1283
  elements.sort((a, b) => a.index - b.index);
1284
1284
  for (const el of elements) {
1285
1285
  if (el.type === 'p') {
1286
- const paragraph = this.parseParagraph(el.xml);
1286
+ // Own content only: the paragraphs inside its text boxes, headers and
1287
+ // captions are elements of their own (withoutNestedContent).
1288
+ const paragraph = this.parseParagraph(this.withoutNestedContent(el.xml));
1287
1289
  // Store XML position from original XML for direct updates
1288
1290
  // This enables fast paragraph updates without re-parsing during save()
1289
1291
  if (el.originalXmlPosition) {
@@ -1864,6 +1866,53 @@ class HwpxParser {
1864
1866
  }
1865
1867
  return paragraphs;
1866
1868
  }
1869
+ /**
1870
+ * The paragraph with every nested container (table, text box, drawing
1871
+ * object, header/footer, caption, equation, note…) cut out, leaving only its
1872
+ * own runs and text. Section paragraphs are read from this: the paragraphs
1873
+ * inside those containers are listed as elements of their own, and the save
1874
+ * path writes a paragraph's text into exactly these own <hp:t>
1875
+ * (HwpxDocument.ownRunText). Reading the whole paragraph instead ended its
1876
+ * run at the first nested </hp:run>: a paragraph "[text box] own text" read
1877
+ * as the text box's text, its own text was never seen, and a whole-paragraph
1878
+ * edit reopened as the old text box text (CodeRabbit, PR #17; 41 of 16,733
1879
+ * paragraphs with own text in 275 Hancom originals lost it on reading).
1880
+ */
1881
+ static withoutNestedContent(paragraphXml) {
1882
+ const nested = /<hp:(tbl|subList|equation|pic|rect|ellipse|polygon|curve|arc|line|container|drawText|textart|ole|footNote|endNote|header|footer)\b/;
1883
+ const openEnd = paragraphXml.indexOf('>') + 1;
1884
+ let rest = paragraphXml.slice(openEnd);
1885
+ let own = '';
1886
+ for (;;) {
1887
+ const m = rest.match(nested);
1888
+ if (!m || m.index === undefined) {
1889
+ own += rest;
1890
+ break;
1891
+ }
1892
+ own += rest.slice(0, m.index);
1893
+ const re = new RegExp(`<(/?)hp:${m[1]}\\b[^>]*?(/?)>`, 'g');
1894
+ re.lastIndex = m.index;
1895
+ let depth = 0;
1896
+ let end = rest.length;
1897
+ let t;
1898
+ while ((t = re.exec(rest)) !== null) {
1899
+ if (t[2]) {
1900
+ if (depth === 0) {
1901
+ end = t.index + t[0].length;
1902
+ break;
1903
+ }
1904
+ continue;
1905
+ }
1906
+ depth += t[1] ? -1 : 1;
1907
+ if (depth === 0) {
1908
+ end = t.index + t[0].length;
1909
+ break;
1910
+ }
1911
+ }
1912
+ rest = rest.slice(end);
1913
+ }
1914
+ return paragraphXml.slice(0, openEnd) + own;
1915
+ }
1867
1916
  static parseParagraph(xml) {
1868
1917
  // Extract only the opening <hp:p ...> tag to get paragraph attributes
1869
1918
  const pTagMatch = xml.match(/^<hp:p\s+([^>]*)>/);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * MCP tool-result envelope and argument checks, kept out of index.ts so they
3
+ * can be tested without starting the stdio server.
4
+ */
5
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
6
+ export declare function success(data: unknown): CallToolResult;
7
+ export declare function error(message: string): CallToolResult;
8
+ /**
9
+ * The declared-required arguments of `toolName` that `args` leaves undefined or
10
+ * null. Reporting them by name replaced a generic handler failure: omitting
11
+ * `section_index` used to surface as "Failed to insert paragraph", which reads
12
+ * like document corruption.
13
+ */
14
+ export declare function findMissingArgs(requiredByTool: ReadonlyMap<string, readonly string[]>, toolName: string, args: Record<string, unknown> | undefined): string[];
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.success = success;
4
+ exports.error = error;
5
+ exports.findMissingArgs = findMissingArgs;
6
+ function success(data) {
7
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
8
+ }
9
+ function error(message) {
10
+ // isError tells the MCP client the call failed. Without it, a missing
11
+ // argument or a refused write came back as a normal result whose body merely
12
+ // contained {"error": …}, and agents treated it as success (reported
13
+ // 2026-09-24). The JSON body is kept for clients that read it.
14
+ return { content: [{ type: 'text', text: JSON.stringify({ error: message }) }], isError: true };
15
+ }
16
+ /**
17
+ * The declared-required arguments of `toolName` that `args` leaves undefined or
18
+ * null. Reporting them by name replaced a generic handler failure: omitting
19
+ * `section_index` used to surface as "Failed to insert paragraph", which reads
20
+ * like document corruption.
21
+ */
22
+ function findMissingArgs(requiredByTool, toolName, args) {
23
+ const required = requiredByTool.get(toolName);
24
+ if (!required || required.length === 0)
25
+ return [];
26
+ return required.filter(key => args?.[key] === undefined || args?.[key] === null);
27
+ }
@@ -0,0 +1,5 @@
1
+ import type JSZip from 'jszip';
2
+ /** First well-formedness error in `xml`, or null when it parses. */
3
+ export declare function xmlWellFormednessError(xml: string): string | null;
4
+ /** Every `.xml` / `.hpf` part of the package that does not parse, as "path: error". */
5
+ export declare function findMalformedXmlParts(zip: JSZip): Promise<string[]>;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.xmlWellFormednessError = xmlWellFormednessError;
4
+ exports.findMalformedXmlParts = findMalformedXmlParts;
5
+ /**
6
+ * Well-formedness check for the XML parts of a saved HWPX package.
7
+ *
8
+ * save_document(verify_integrity) used to look for three textual symptoms
9
+ * (no `<?xml`, a dangling `<` at the end, `<` inside a tag). A section with a
10
+ * mismatched close tag passed all three and was reported as
11
+ * `integrity_verified: true`, while Hancom and every XML parser rejected it
12
+ * (reported 2026-09-24: `<hp:p>` 5730 open / 5728 close after one edit).
13
+ * A real parser is the only check that means what the flag claims.
14
+ *
15
+ * Measured on 275 Hancom-saved originals (2,046 XML parts, 168 MB): 0 false
16
+ * rejections, ~1.5 s total. The only rejected sample was a password-protected
17
+ * file, whose parts are ciphertext rather than XML.
18
+ */
19
+ const saxes_1 = require("saxes");
20
+ /** First well-formedness error in `xml`, or null when it parses. */
21
+ function xmlWellFormednessError(xml) {
22
+ // xmlns: true also rejects an undeclared prefix (<hs:sec> with only
23
+ // xmlns:hp declared). Hancom declares every prefix it uses (0 rejections
24
+ // across the 275-file corpus with this setting), while set_section_xml
25
+ // accepted a section missing xmlns:hs.
26
+ const parser = new saxes_1.SaxesParser({ xmlns: true });
27
+ let first = null;
28
+ parser.on('error', err => {
29
+ if (first === null)
30
+ first = err.message;
31
+ });
32
+ try {
33
+ parser.write(xml).close();
34
+ }
35
+ catch (err) {
36
+ first ?? (first = err instanceof Error ? err.message : String(err));
37
+ }
38
+ return first;
39
+ }
40
+ /** Every `.xml` / `.hpf` part of the package that does not parse, as "path: error". */
41
+ async function findMalformedXmlParts(zip) {
42
+ const out = [];
43
+ const names = Object.keys(zip.files)
44
+ .filter(name => !zip.files[name].dir && /\.(xml|hpf)$/i.test(name))
45
+ .sort();
46
+ for (const name of names) {
47
+ const err = xmlWellFormednessError(await zip.file(name).async('string'));
48
+ if (err)
49
+ out.push(`${name}: ${err}`);
50
+ }
51
+ return out;
52
+ }