@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.
- package/CHANGELOG.md +100 -0
- package/README.md +13 -3
- package/dist/HwpxDocument.d.ts +139 -0
- package/dist/HwpxDocument.js +938 -231
- package/dist/HwpxParser.d.ts +13 -0
- package/dist/HwpxParser.js +50 -1
- package/dist/ToolResult.d.ts +14 -0
- package/dist/ToolResult.js +27 -0
- package/dist/XmlWellFormed.d.ts +5 -0
- package/dist/XmlWellFormed.js +52 -0
- package/dist/index.js +455 -473
- package/package.json +8 -2
package/dist/HwpxParser.d.ts
CHANGED
|
@@ -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;
|
package/dist/HwpxParser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|