refactor(mcp-server): make the diagram modules usable from the web app

The web app will reuse the MCP server's XML engine instead of its own
copy in lib/utils.ts, so these modules now run in the browser too.

- Relative imports end in .ts, rewritten to .js by tsc
  (rewriteRelativeImportExtensions); Next.js resolves them directly
- Every module uses the global DOMParser/XMLSerializer: native in the
  browser, linkedom in Node via installDomPolyfill. pages.ts parsed with
  linkedom but serialized with the global serializer, which throws in
  the browser
- The saxes syntax check moves to xml-syntax.ts, so the browser does not
  pull in linkedom; it now also rejects undeclared prefixes such as
  xlink:, as the browser does
- Page decompression uses pako and atob instead of node:zlib and Buffer
- hasCells moves to pages.ts, away from the file system code
- The duplicate cell id check counts UserObject/object ids
- wrapCellsInModel drops comments and text before the first cell, which
  the web app accepts today
- validateAndFixXml takes { strict: false } for diagrams with user content
- Web tests run these modules with a browser DOM (jsdom)
- saxes becomes a direct dependency of the web app
This commit is contained in:
dayuan.jiang
2026-10-04 12:25:53 +09:00
parent 289e6bc25f
commit b7c543ca70
29 changed files with 287 additions and 98 deletions
+19 -7
View File
@@ -17,8 +17,7 @@
* - how to add/rename/delete pages without re-parsing ad-hoc.
*/
import { DOMParser } from "linkedom"
import { getXmlSyntaxError } from "./dom.js"
import { getXmlSyntaxError } from "./xml-syntax.ts"
export interface PageInfo {
id: string
@@ -53,6 +52,10 @@ export function generatePageId(): string {
return `${a}-${b}`
}
/** Any cell besides the root cells "0" and "1" */
export const hasCells = (xml: string) =>
/<(mxCell\b[^>]*\bid="(?![01]")|UserObject\b|object\b)/.test(xml)
/** Cheap regex check — does the XML start with an <mxfile> root? */
export function isMxFile(xml: string): boolean {
return /^\s*(<\?xml[^>]*\?>\s*)?<mxfile[\s>]/i.test(xml)
@@ -87,15 +90,24 @@ const ROOT_CELLS = '<mxCell id="0"/><mxCell id="1" parent="0"/>'
* Turn a list of bare cells (optionally inside <root>) into a one-page
* <mxGraphModel>, adding the "0" and "1" root cells. The model then only
* writes its own cells, as in the web app (wrapWithMxFile in lib/utils.ts).
* Root cells the model wrote anyway are replaced, and trailing closing tags
* some providers append are dropped. <mxfile>, <mxGraphModel> and anything
* else are returned unchanged.
* Root cells the model wrote anyway are replaced, and comments or text
* before the first cell and trailing closing tags some providers append
* are dropped. <mxfile>, <mxGraphModel> and anything else are returned
* unchanged.
*/
export function wrapCellsInModel(xml: string): string {
let content = stripXmlDeclaration(xml.trim())
if (!/^<(mxCell|UserObject|object|root)[\s/>]/.test(content)) return xml
const start = content.search(/<(mxCell|UserObject|object|root)[\s/>]/)
if (start === -1) return xml
// Only comments and plain text may come before the first cell
if (!/^(?:<!--[\s\S]*?-->|[^<])*$/.test(content.slice(0, start))) {
return xml
}
content = content.replace(/<\/?root>/g, "").trim()
content = content
.slice(start)
.replace(/<\/?root>/g, "")
.trim()
// End of the last cell, counting wrapped cells (</UserObject>, </object>)
let end = -1
for (const close of ["/>", "</mxCell>", "</UserObject>", "</object>"]) {