{"version":3,"file":"import-CYMa-7i4.mjs","names":["normalizeUrl"],"sources":["../src/cli/wxr/parser.ts","../src/import/menus.ts","../src/import/sections.ts","../src/import/comments.ts","../src/import/settings.ts","../src/import/registry.ts","../src/import/sources/wxr.ts","../src/import/sources/wordpress-rest.ts","../src/import/sources/wordpress-plugin.ts","../src/import/index.ts"],"sourcesContent":["/**\n * WordPress WXR (WordPress eXtended RSS) parser\n *\n * Uses SAX streaming parser to handle large export files efficiently.\n * WXR is an RSS extension containing WordPress content exports.\n *\n * @see https://developer.wordpress.org/plugins/data-storage/wp-xml-rpc/\n */\n\nimport type { Readable } from \"node:stream\";\n\nimport sax from \"sax\";\n\n// Regex patterns for WXR parsing\nconst PHP_SERIALIZED_STRING_PATTERN = /s:\\d+:\"([^\"]+)\"/g;\nconst PHP_SERIALIZED_STRING_MATCH_PATTERN = /s:\\d+:\"([^\"]+)\"/;\n\n/**\n * Parsed WordPress export data\n */\nexport interface WxrData {\n\t/** Site metadata */\n\tsite: WxrSite;\n\t/** Posts (including custom post types) */\n\tposts: WxrPost[];\n\t/** Media attachments */\n\tattachments: WxrAttachment[];\n\t/** Categories */\n\tcategories: WxrCategory[];\n\t/** Tags */\n\ttags: WxrTag[];\n\t/** Authors */\n\tauthors: WxrAuthor[];\n\t/** All taxonomy terms (including custom taxonomies and nav_menu) */\n\tterms: WxrTerm[];\n\t/** Parsed navigation menus */\n\tnavMenus: WxrNavMenu[];\n}\n\nexport interface WxrSite {\n\ttitle?: string;\n\tlink?: string;\n\tdescription?: string;\n\tlanguage?: string;\n\tbaseSiteUrl?: string;\n\tbaseBlogUrl?: string;\n}\n\nexport interface WxrPost {\n\tid?: number;\n\ttitle?: string;\n\tlink?: string;\n\tpubDate?: string;\n\tcreator?: string;\n\tguid?: string;\n\tdescription?: string;\n\tcontent?: string;\n\texcerpt?: string;\n\tpostDate?: string;\n\tpostDateGmt?: string;\n\tpostModified?: string;\n\tpostModifiedGmt?: string;\n\tcommentStatus?: string;\n\tpingStatus?: string;\n\tstatus?: string;\n\tpostType?: string;\n\tpostName?: string;\n\tpostPassword?: string;\n\tisSticky?: boolean;\n\t/** Parent post ID for hierarchical content (pages) */\n\tpostParent?: number;\n\t/** Menu order for sorting */\n\tmenuOrder?: number;\n\tcategories: string[];\n\ttags: string[];\n\t/** Custom taxonomy assignments beyond categories/tags */\n\tcustomTaxonomies?: Map<string, string[]>;\n\t/**\n\t * Display labels for per-item category/tag/custom-taxonomy assignments,\n\t * captured from `<category domain=\"...\" nicename=\"...\">Label</category>`\n\t * text content. Used by the importer to back-fill term labels when a\n\t * `<wp:category>` or `<wp:tag>` block wasn't present at the top of the\n\t * WXR (older / hand-edited exports). Keyed by\n\t * `${normalisedTaxonomy}\\u0000${slug}` so categories and tags don't\n\t * collide.\n\t */\n\ttaxonomyLabels?: Map<string, string>;\n\tmeta: Map<string, string>;\n\t/**\n\t * BCP 47 locale code extracted from a detected multilingual plugin\n\t * (WPML's `_icl_lang_code` or Polylang's per-post language taxonomy).\n\t * Absent when no per-post locale could be determined.\n\t */\n\tlocale?: string;\n\t/**\n\t * Source-side translation group ID extracted from a detected multilingual\n\t * plugin (WPML's `trid` / `_icl_translation_id`, or a synthesized id derived\n\t * from Polylang's `_translations` meta). Posts sharing a `translationGroup`\n\t * are translations of one another. The string is opaque -- consumers use it\n\t * only as a key to link posts.\n\t */\n\ttranslationGroup?: string;\n}\n\n/**\n * WPML stores per-post language in postmeta as `_icl_lang_code`. The shared\n * translation id is `trid` (this is the group ID -- every translation of the\n * same content shares it). `_icl_translation_id` exists on some exports too\n * but is a per-translation row id from `wp_icl_translations`, NOT the group\n * id, so it must NOT be used as the group key. We accept it only when `trid`\n * is absent and trust the export to be internally consistent (the only case\n * where that's reasonable is single-post exports with no real grouping).\n *\n * See `wpml_element_trid` in the WPML hook docs: \"the ID of the translation\n * group\".\n */\nconst WPML_LOCALE_META_KEYS = [\"_icl_lang_code\"] as const;\nconst WPML_TRID_META_KEYS = [\"trid\", \"_icl_translation_id\"] as const;\n\n/**\n * Polylang stores per-post language in postmeta as `_locale` on newer\n * exports. The actual language taxonomy assignment lives on\n * `customTaxonomies.language`, which we use as a fallback. Translation\n * grouping is encoded in `_translations` as a serialized PHP map of\n * `{ lang_code => post_id }`; we synthesize a stable group key from the\n * sorted IDs so every member of the group resolves to the same string.\n */\nconst POLYLANG_LOCALE_META_KEY = \"_locale\";\nconst POLYLANG_TRANSLATIONS_META_KEY = \"_translations\";\nconst POLYLANG_LANG_TAXONOMY = \"language\";\n\n/**\n * Extract a list of post-IDs from Polylang's `_translations` PHP-serialized\n * value. The format we care about is roughly:\n *\n *   a:2:{s:2:\"en\";i:1;s:2:\"ar\";i:7;}\n *\n * We don't need to round-trip the PHP value -- we just need a stable group\n * key shared by every translation of the same content. Concatenating the\n * sorted IDs gives us exactly that: every post in the group derives the\n * same key from its own copy of `_translations`.\n *\n * Naïve `/i:(\\d+);/g` would also match `i:N;` literals embedded INSIDE\n * string values (e.g. `s:11:\"i:42;hello\";`), which would silently corrupt\n * the group key. We walk the serialized blob token-by-token instead.\n *\n * PHP serializes `s:LEN:\"...\"` with LEN counted in BYTES, not characters\n * (UTF-8 byte length). JS string positions are UTF-16 code units, so we\n * encode to bytes via `TextEncoder` and walk byte offsets. Single-byte-only\n * inputs (the common case for Polylang's `_translations` which only stores\n * ASCII locale codes) take the same path; the encoder is cheap.\n */\nfunction polylangTranslationGroupFromMeta(serialized: string): string | undefined {\n\t// Operate on the UTF-8 byte view so `s:LEN:` advances the right number\n\t// of bytes when the payload contains multibyte characters.\n\tconst encoder = new TextEncoder();\n\tconst bytes = encoder.encode(serialized);\n\tconst decoder = new TextDecoder(\"utf-8\");\n\n\tconst ids: number[] = [];\n\tlet i = 0;\n\tconst n = bytes.length;\n\tconst CHAR_S = 0x73; // 's'\n\tconst CHAR_I = 0x69; // 'i'\n\tconst CHAR_COLON = 0x3a; // ':'\n\tconst CHAR_SEMI = 0x3b; // ';'\n\tconst CHAR_QUOTE = 0x22; // '\"'\n\n\tconst indexOf = (byte: number, from: number): number => {\n\t\tfor (let k = from; k < n; k++) {\n\t\t\tif (bytes[k] === byte) return k;\n\t\t}\n\t\treturn -1;\n\t};\n\n\twhile (i < n) {\n\t\tconst ch = bytes[i];\n\t\tif (ch === CHAR_S && bytes[i + 1] === CHAR_COLON) {\n\t\t\t// s:LEN:\"...\";  — skip the entire string value: the LEN digits,\n\t\t\t// the opening quote, the payload of exactly LEN bytes, the\n\t\t\t// closing quote, and the trailing semicolon.\n\t\t\tconst lenStart = i + 2;\n\t\t\tconst lenEnd = indexOf(CHAR_COLON, lenStart);\n\t\t\tif (lenEnd === -1) break;\n\t\t\tconst lenText = decoder.decode(bytes.slice(lenStart, lenEnd));\n\t\t\tconst len = Number.parseInt(lenText, 10);\n\t\t\tif (!Number.isFinite(len) || len < 0) {\n\t\t\t\ti = lenEnd + 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// lenEnd+1 should be `\"`. Defensively check before advancing\n\t\t\t// over the payload -- a malformed input shouldn't crash the\n\t\t\t// import; just skip past this token.\n\t\t\tif (bytes[lenEnd + 1] !== CHAR_QUOTE) {\n\t\t\t\ti = lenEnd + 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst payloadStart = lenEnd + 2;\n\t\t\tconst afterPayload = payloadStart + len;\n\t\t\t// afterPayload should point at the closing `\"`; +2 past `\";`.\n\t\t\ti = afterPayload + 2;\n\t\t\tcontinue;\n\t\t}\n\t\tif (ch === CHAR_I && bytes[i + 1] === CHAR_COLON) {\n\t\t\tconst valStart = i + 2;\n\t\t\tconst valEnd = indexOf(CHAR_SEMI, valStart);\n\t\t\tif (valEnd === -1) break;\n\t\t\tconst idText = decoder.decode(bytes.slice(valStart, valEnd));\n\t\t\tconst id = Number.parseInt(idText, 10);\n\t\t\tif (Number.isFinite(id)) ids.push(id);\n\t\t\ti = valEnd + 1;\n\t\t\tcontinue;\n\t\t}\n\t\t// Any other token (a:LEN:{...}, b:0;, N;, {, }, :, ;, etc.) -- just\n\t\t// advance one byte. We only care about integer literals; everything\n\t\t// else is structural.\n\t\ti++;\n\t}\n\tif (ids.length === 0) return undefined;\n\tconst sorted = [...new Set(ids)].toSorted((a, b) => a - b);\n\treturn `pll:${sorted.join(\",\")}`;\n}\n\n/**\n * Promote multilingual-plugin metadata from `post.meta` and\n * `post.customTaxonomies` into `post.locale` / `post.translationGroup`.\n *\n * Called once per `<item>` after all of its `<wp:postmeta>` and per-item\n * `<category>` entries have been parsed. Safe to call on posts that have no\n * multilingual metadata -- it's a no-op in that case.\n *\n * WPML wins over Polylang when both are present (they shouldn't co-exist on\n * the same site, but defensive precedence avoids ambiguity).\n */\nfunction promoteI18nMetadata(post: WxrPost): void {\n\t// WPML\n\tfor (const key of WPML_LOCALE_META_KEYS) {\n\t\tconst value = post.meta.get(key);\n\t\tif (value) {\n\t\t\tpost.locale = value;\n\t\t\tbreak;\n\t\t}\n\t}\n\tfor (const key of WPML_TRID_META_KEYS) {\n\t\tconst value = post.meta.get(key);\n\t\tif (value) {\n\t\t\tpost.translationGroup = `wpml:${value}`;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Polylang fallbacks (only fill what WPML didn't already provide)\n\tif (!post.locale) {\n\t\tconst pllLocale = post.meta.get(POLYLANG_LOCALE_META_KEY);\n\t\tif (pllLocale) {\n\t\t\tpost.locale = pllLocale;\n\t\t} else {\n\t\t\t// Polylang's primary language signal is a taxonomy assignment\n\t\t\t// on the `language` custom taxonomy. The nicename is the locale\n\t\t\t// code (e.g. \"en\", \"ar\").\n\t\t\tconst langTaxonomy = post.customTaxonomies?.get(POLYLANG_LANG_TAXONOMY);\n\t\t\tconst firstLang = langTaxonomy?.[0];\n\t\t\tif (firstLang) post.locale = firstLang;\n\t\t}\n\t}\n\n\tif (!post.translationGroup) {\n\t\tconst pllTranslations = post.meta.get(POLYLANG_TRANSLATIONS_META_KEY);\n\t\tif (pllTranslations) {\n\t\t\tconst group = polylangTranslationGroupFromMeta(pllTranslations);\n\t\t\tif (group) post.translationGroup = group;\n\t\t}\n\t}\n}\n\nexport interface WxrAttachment {\n\tid?: number;\n\ttitle?: string;\n\turl?: string;\n\tpostDate?: string;\n\tmeta: Map<string, string>;\n}\n\nexport interface WxrCategory {\n\tid?: number;\n\tnicename?: string;\n\tname?: string;\n\tparent?: string;\n\tdescription?: string;\n}\n\nexport interface WxrTag {\n\tid?: number;\n\tslug?: string;\n\tname?: string;\n\tdescription?: string;\n}\n\n/**\n * Generic taxonomy term (categories, tags, nav_menu, custom taxonomies)\n */\nexport interface WxrTerm {\n\tid: number;\n\ttaxonomy: string; // 'category', 'post_tag', 'nav_menu', 'genre', etc.\n\tslug: string;\n\tname: string;\n\tparent?: string;\n\tdescription?: string;\n}\n\n/**\n * Navigation menu structure\n */\nexport interface WxrNavMenu {\n\tid: number;\n\tname: string; // Menu slug\n\tlabel: string; // Menu name\n\titems: WxrNavMenuItem[];\n}\n\n/**\n * Navigation menu item\n */\nexport interface WxrNavMenuItem {\n\tid: number;\n\tmenuId: number;\n\tparentId?: number;\n\tsortOrder: number;\n\ttype: \"custom\" | \"post_type\" | \"taxonomy\";\n\tobjectType?: string; // 'page', 'post', 'category'\n\tobjectId?: number;\n\turl?: string;\n\ttitle: string;\n\ttarget?: string;\n\tclasses?: string;\n}\n\nexport interface WxrAuthor {\n\tid?: number;\n\tlogin?: string;\n\temail?: string;\n\tdisplayName?: string;\n\tfirstName?: string;\n\tlastName?: string;\n}\n\n/** Extract string value from a SAX attribute (handles both Tag and QualifiedTag) */\nfunction attrStr(attr: string | { value: string } | undefined): string {\n\tif (typeof attr === \"string\") return attr;\n\tif (attr && typeof attr === \"object\" && \"value\" in attr) return attr.value;\n\treturn \"\";\n}\n\n/**\n * Normalise a `<category domain=\"...\">` value to the matching EmDash\n * taxonomy name so per-item label captures can be retrieved later using\n * the same key.\n */\nfunction normaliseDomain(domain: string): string {\n\tif (domain === \"post_tag\") return \"tag\";\n\treturn domain;\n}\n\n/**\n * Persist the human label of a `<category>` text body keyed by the\n * normalised `(taxonomy, slug)` pair. Skips trivial labels that equal the\n * slug (no information vs. just storing the slug).\n */\nfunction captureItemCategoryLabel(\n\titem: WxrPost,\n\tpair: { domain: string; nicename: string },\n\tlabel: string,\n): void {\n\tif (!label || label === pair.nicename) return;\n\tif (!item.taxonomyLabels) item.taxonomyLabels = new Map();\n\tconst key = `${normaliseDomain(pair.domain)}\\u0000${pair.nicename}`;\n\tif (!item.taxonomyLabels.has(key)) item.taxonomyLabels.set(key, label);\n}\n\n/** Type guard for complete WxrTerm (all required fields present) */\nfunction isCompleteWxrTerm(term: Partial<WxrTerm>): term is WxrTerm {\n\treturn (\n\t\tterm.id !== undefined &&\n\t\tterm.taxonomy !== undefined &&\n\t\tterm.slug !== undefined &&\n\t\tterm.name !== undefined\n\t);\n}\n\n/**\n * Parse a WordPress WXR export file\n */\nexport function parseWxr(stream: Readable): Promise<WxrData> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst parser = sax.createStream(true, { trim: true });\n\n\t\tconst data: WxrData = {\n\t\t\tsite: {},\n\t\t\tposts: [],\n\t\t\tattachments: [],\n\t\t\tcategories: [],\n\t\t\ttags: [],\n\t\t\tauthors: [],\n\t\t\tterms: [],\n\t\t\tnavMenus: [],\n\t\t};\n\n\t\t// Parser state\n\t\tlet currentPath: string[] = [];\n\t\tlet currentText = \"\";\n\t\tlet currentItem: WxrPost | null = null;\n\t\tlet currentAttachment: WxrAttachment | null = null;\n\t\tlet currentCategory: WxrCategory | null = null;\n\t\tlet currentTag: WxrTag | null = null;\n\t\tlet currentAuthor: WxrAuthor | null = null;\n\t\tlet currentTerm: Partial<WxrTerm> | null = null;\n\t\tlet currentMetaKey = \"\";\n\t\t// Per-item category element currently open. Captured at opentag so\n\t\t// we can pair the text body (the human label) with the slug when\n\t\t// closetag fires. WXR per-item category elements look like:\n\t\t//   <category domain=\"category\" nicename=\"hello-world\">Hello World</category>\n\t\tlet pendingItemCategory: { domain: string; nicename: string } | null = null;\n\n\t\t// Track nav_menu_item posts for post-processing\n\t\tconst navMenuItemPosts: WxrPost[] = [];\n\t\t// Track menu term IDs by slug for linking items to menus\n\t\tconst menuTermsBySlug = new Map<string, number>();\n\n\t\tparser.on(\"opentag\", (node) => {\n\t\t\tconst tagName = node.name.toLowerCase();\n\t\t\tcurrentPath.push(tagName);\n\t\t\tcurrentText = \"\";\n\n\t\t\t// Start new item\n\t\t\tif (tagName === \"item\") {\n\t\t\t\tcurrentItem = {\n\t\t\t\t\tcategories: [],\n\t\t\t\t\ttags: [],\n\t\t\t\t\tcustomTaxonomies: new Map(),\n\t\t\t\t\tmeta: new Map(),\n\t\t\t\t};\n\t\t\t} else if (tagName === \"wp:category\") {\n\t\t\t\tcurrentCategory = {};\n\t\t\t} else if (tagName === \"wp:tag\") {\n\t\t\t\tcurrentTag = {};\n\t\t\t} else if (tagName === \"wp:author\") {\n\t\t\t\tcurrentAuthor = {};\n\t\t\t} else if (tagName === \"wp:term\") {\n\t\t\t\tcurrentTerm = {};\n\t\t\t}\n\n\t\t\t// Handle category/tag/custom taxonomy assignment in items\n\t\t\tif (tagName === \"category\" && currentItem && node.attributes) {\n\t\t\t\tconst domain = attrStr(node.attributes.domain);\n\t\t\t\tconst nicename = attrStr(node.attributes.nicename);\n\t\t\t\tif (domain === \"category\" && nicename) {\n\t\t\t\t\tcurrentItem.categories.push(nicename);\n\t\t\t\t\tpendingItemCategory = { domain, nicename };\n\t\t\t\t} else if (domain === \"post_tag\" && nicename) {\n\t\t\t\t\tcurrentItem.tags.push(nicename);\n\t\t\t\t\tpendingItemCategory = { domain, nicename };\n\t\t\t\t} else if (domain && nicename && domain !== \"category\" && domain !== \"post_tag\") {\n\t\t\t\t\t// Custom taxonomy (including nav_menu)\n\t\t\t\t\tif (!currentItem.customTaxonomies) {\n\t\t\t\t\t\tcurrentItem.customTaxonomies = new Map();\n\t\t\t\t\t}\n\t\t\t\t\tconst existing = currentItem.customTaxonomies.get(domain) || [];\n\t\t\t\t\texisting.push(nicename);\n\t\t\t\t\tcurrentItem.customTaxonomies.set(domain, existing);\n\t\t\t\t\tpendingItemCategory = { domain, nicename };\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tparser.on(\"text\", (text) => {\n\t\t\tcurrentText += text;\n\t\t});\n\n\t\tparser.on(\"cdata\", (cdata) => {\n\t\t\tcurrentText += cdata;\n\t\t});\n\n\t\tparser.on(\"closetag\", (tagName) => {\n\t\t\tconst tag = tagName.toLowerCase();\n\t\t\tconst text = currentText.trim();\n\n\t\t\t// Site-level metadata (in channel)\n\t\t\tif (currentPath.includes(\"channel\") && !currentItem) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"title\":\n\t\t\t\t\t\tif (!data.site.title) data.site.title = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"link\":\n\t\t\t\t\t\tif (!data.site.link) data.site.link = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"description\":\n\t\t\t\t\t\tif (!data.site.description) data.site.description = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"language\":\n\t\t\t\t\t\tdata.site.language = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:base_site_url\":\n\t\t\t\t\t\tdata.site.baseSiteUrl = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:base_blog_url\":\n\t\t\t\t\t\tdata.site.baseBlogUrl = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Item (post/page/attachment) parsing\n\t\t\tif (currentItem) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"title\":\n\t\t\t\t\t\tcurrentItem.title = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"link\":\n\t\t\t\t\t\tcurrentItem.link = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"pubdate\":\n\t\t\t\t\t\tcurrentItem.pubDate = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"dc:creator\":\n\t\t\t\t\t\tcurrentItem.creator = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"guid\":\n\t\t\t\t\t\tcurrentItem.guid = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"description\":\n\t\t\t\t\t\tcurrentItem.description = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"content:encoded\":\n\t\t\t\t\t\tcurrentItem.content = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"excerpt:encoded\":\n\t\t\t\t\t\tcurrentItem.excerpt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_id\":\n\t\t\t\t\t\tcurrentItem.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_date\":\n\t\t\t\t\t\tcurrentItem.postDate = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_date_gmt\":\n\t\t\t\t\t\tcurrentItem.postDateGmt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_modified\":\n\t\t\t\t\t\tcurrentItem.postModified = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_modified_gmt\":\n\t\t\t\t\t\tcurrentItem.postModifiedGmt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:comment_status\":\n\t\t\t\t\t\tcurrentItem.commentStatus = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:ping_status\":\n\t\t\t\t\t\tcurrentItem.pingStatus = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:status\":\n\t\t\t\t\t\tcurrentItem.status = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_type\":\n\t\t\t\t\t\tcurrentItem.postType = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_name\":\n\t\t\t\t\t\tcurrentItem.postName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_parent\":\n\t\t\t\t\t\tcurrentItem.postParent = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:menu_order\":\n\t\t\t\t\t\tcurrentItem.menuOrder = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_password\":\n\t\t\t\t\t\tcurrentItem.postPassword = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:is_sticky\":\n\t\t\t\t\t\tcurrentItem.isSticky = text === \"1\";\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:meta_key\":\n\t\t\t\t\t\tcurrentMetaKey = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:meta_value\":\n\t\t\t\t\t\tif (currentMetaKey) {\n\t\t\t\t\t\t\tcurrentItem.meta.set(currentMetaKey, text);\n\t\t\t\t\t\t\tcurrentMetaKey = \"\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:attachment_url\":\n\t\t\t\t\t\tif (currentItem.postType === \"attachment\") {\n\t\t\t\t\t\t\t// This is actually an attachment\n\t\t\t\t\t\t\tcurrentAttachment = {\n\t\t\t\t\t\t\t\tid: currentItem.id,\n\t\t\t\t\t\t\t\ttitle: currentItem.title,\n\t\t\t\t\t\t\t\turl: text,\n\t\t\t\t\t\t\t\tpostDate: currentItem.postDate,\n\t\t\t\t\t\t\t\tmeta: currentItem.meta,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"category\":\n\t\t\t\t\t\t// Per-item category text body = the human label for\n\t\t\t\t\t\t// the term (`<category nicename=\"hello-world\">Hello\n\t\t\t\t\t\t// World</category>`). Backfilling from per-item\n\t\t\t\t\t\t// elements (older / hand-edited exports without top-\n\t\t\t\t\t\t// level `<wp:category>` blocks) lands the right label\n\t\t\t\t\t\t// instead of slug-cased nonsense.\n\t\t\t\t\t\tif (pendingItemCategory && text) {\n\t\t\t\t\t\t\tcaptureItemCategoryLabel(currentItem, pendingItemCategory, text);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpendingItemCategory = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"item\":\n\t\t\t\t\t\t// End of item - categorize and store\n\t\t\t\t\t\tif (currentAttachment) {\n\t\t\t\t\t\t\tdata.attachments.push(currentAttachment);\n\t\t\t\t\t\t\tcurrentAttachment = null;\n\t\t\t\t\t\t} else if (currentItem.postType === \"nav_menu_item\") {\n\t\t\t\t\t\t\t// Track nav_menu_item posts for post-processing into menus\n\t\t\t\t\t\t\tnavMenuItemPosts.push(currentItem);\n\t\t\t\t\t\t\tdata.posts.push(currentItem);\n\t\t\t\t\t\t} else if (currentItem.postType !== \"attachment\") {\n\t\t\t\t\t\t\t// Promote multilingual plugin metadata before storing.\n\t\t\t\t\t\t\t// All postmeta and per-item categories are parsed by the time\n\t\t\t\t\t\t\t// the closing </item> tag fires, so it's safe to inspect them.\n\t\t\t\t\t\t\tpromoteI18nMetadata(currentItem);\n\t\t\t\t\t\t\t// Store all non-attachment post types (posts, pages, custom post types)\n\t\t\t\t\t\t\tdata.posts.push(currentItem);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentItem = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Category parsing\n\t\t\tif (currentCategory) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentCategory.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_nicename\":\n\t\t\t\t\t\tcurrentCategory.nicename = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:cat_name\":\n\t\t\t\t\t\tcurrentCategory.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_parent\":\n\t\t\t\t\t\tcurrentCategory.parent = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_description\":\n\t\t\t\t\t\tcurrentCategory.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category\":\n\t\t\t\t\t\tif (currentCategory.name) {\n\t\t\t\t\t\t\tdata.categories.push(currentCategory);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentCategory = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Tag parsing\n\t\t\tif (currentTag) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentTag.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_slug\":\n\t\t\t\t\t\tcurrentTag.slug = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_name\":\n\t\t\t\t\t\tcurrentTag.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_description\":\n\t\t\t\t\t\tcurrentTag.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag\":\n\t\t\t\t\t\tif (currentTag.name) {\n\t\t\t\t\t\t\tdata.tags.push(currentTag);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentTag = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Author parsing\n\t\t\tif (currentAuthor) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:author_id\":\n\t\t\t\t\t\tcurrentAuthor.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_login\":\n\t\t\t\t\t\tcurrentAuthor.login = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_email\":\n\t\t\t\t\t\tcurrentAuthor.email = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_display_name\":\n\t\t\t\t\t\tcurrentAuthor.displayName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_first_name\":\n\t\t\t\t\t\tcurrentAuthor.firstName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_last_name\":\n\t\t\t\t\t\tcurrentAuthor.lastName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author\":\n\t\t\t\t\t\tif (currentAuthor.login) {\n\t\t\t\t\t\t\tdata.authors.push(currentAuthor);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentAuthor = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Generic term parsing (wp:term elements - custom taxonomies, nav_menu, etc.)\n\t\t\tif (currentTerm) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentTerm.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_taxonomy\":\n\t\t\t\t\t\tcurrentTerm.taxonomy = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_slug\":\n\t\t\t\t\t\tcurrentTerm.slug = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_name\":\n\t\t\t\t\t\tcurrentTerm.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_parent\":\n\t\t\t\t\t\tcurrentTerm.parent = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_description\":\n\t\t\t\t\t\tcurrentTerm.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term\":\n\t\t\t\t\t\tif (isCompleteWxrTerm(currentTerm)) {\n\t\t\t\t\t\t\tdata.terms.push(currentTerm);\n\t\t\t\t\t\t\t// Track nav_menu terms for building menus\n\t\t\t\t\t\t\tif (currentTerm.taxonomy === \"nav_menu\") {\n\t\t\t\t\t\t\t\tmenuTermsBySlug.set(currentTerm.slug, currentTerm.id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentTerm = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcurrentPath.pop();\n\t\t\tcurrentText = \"\";\n\t\t});\n\n\t\tparser.on(\"error\", (err) => {\n\t\t\treject(new Error(`XML parsing error: ${err.message}`));\n\t\t});\n\n\t\tparser.on(\"end\", () => {\n\t\t\t// Post-process nav_menu_item posts into structured menus\n\t\t\tdata.navMenus = buildNavMenus(navMenuItemPosts, menuTermsBySlug);\n\t\t\tresolve(data);\n\t\t});\n\n\t\t// Pipe the stream through the parser\n\t\tstream.pipe(parser);\n\t});\n}\n\n/**\n * Parse a WordPress WXR export from a string\n *\n * Uses the non-streaming SAX parser API for compatibility with\n * environments that don't have Node.js streams (e.g., Cloudflare Workers).\n */\nexport function parseWxrString(xml: string): Promise<WxrData> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst parser = sax.parser(true, { trim: false, normalize: false });\n\n\t\tconst data: WxrData = {\n\t\t\tsite: {},\n\t\t\tposts: [],\n\t\t\tattachments: [],\n\t\t\tcategories: [],\n\t\t\ttags: [],\n\t\t\tauthors: [],\n\t\t\tterms: [],\n\t\t\tnavMenus: [],\n\t\t};\n\n\t\tlet currentPath: string[] = [];\n\t\tlet currentText = \"\";\n\t\tlet currentItem: WxrPost | null = null;\n\t\tlet currentAttachment: WxrAttachment | null = null;\n\t\tlet currentCategory: WxrCategory | null = null;\n\t\tlet currentTag: WxrTag | null = null;\n\t\tlet currentAuthor: WxrAuthor | null = null;\n\t\tlet currentTerm: Partial<WxrTerm> | null = null;\n\t\tlet currentMetaKey = \"\";\n\t\t// Per-item category element currently open (see streaming-parser\n\t\t// counterpart above for rationale).\n\t\tlet pendingItemCategory: { domain: string; nicename: string } | null = null;\n\n\t\t// Track nav_menu_item posts for post-processing\n\t\tconst navMenuItemPosts: WxrPost[] = [];\n\t\t// Track menu term IDs by slug for linking items to menus\n\t\tconst menuTermsBySlug = new Map<string, number>();\n\n\t\tparser.onopentag = (node) => {\n\t\t\tconst tag = node.name.toLowerCase();\n\t\t\tcurrentPath.push(tag);\n\t\t\tcurrentText = \"\";\n\n\t\t\t// Start new elements\n\t\t\tif (tag === \"item\") {\n\t\t\t\tcurrentItem = {\n\t\t\t\t\tcategories: [],\n\t\t\t\t\ttags: [],\n\t\t\t\t\tcustomTaxonomies: new Map(),\n\t\t\t\t\tmeta: new Map(),\n\t\t\t\t};\n\t\t\t} else if (tag === \"wp:category\") {\n\t\t\t\tcurrentCategory = {};\n\t\t\t} else if (tag === \"wp:tag\") {\n\t\t\t\tcurrentTag = {};\n\t\t\t} else if (tag === \"wp:author\") {\n\t\t\t\tcurrentAuthor = {};\n\t\t\t} else if (tag === \"wp:term\") {\n\t\t\t\tcurrentTerm = {};\n\t\t\t}\n\n\t\t\t// Handle category/tag/custom taxonomy assignment in items\n\t\t\tif (tag === \"category\" && currentItem && node.attributes) {\n\t\t\t\tconst domain = attrStr(node.attributes.domain);\n\t\t\t\tconst nicename = attrStr(node.attributes.nicename);\n\t\t\t\tif (domain === \"category\" && nicename) {\n\t\t\t\t\tcurrentItem.categories.push(nicename);\n\t\t\t\t\tpendingItemCategory = { domain, nicename };\n\t\t\t\t} else if (domain === \"post_tag\" && nicename) {\n\t\t\t\t\tcurrentItem.tags.push(nicename);\n\t\t\t\t\tpendingItemCategory = { domain, nicename };\n\t\t\t\t} else if (domain && nicename && domain !== \"category\" && domain !== \"post_tag\") {\n\t\t\t\t\t// Custom taxonomy (including nav_menu)\n\t\t\t\t\tif (!currentItem.customTaxonomies) {\n\t\t\t\t\t\tcurrentItem.customTaxonomies = new Map();\n\t\t\t\t\t}\n\t\t\t\t\tconst existing = currentItem.customTaxonomies.get(domain) || [];\n\t\t\t\t\texisting.push(nicename);\n\t\t\t\t\tcurrentItem.customTaxonomies.set(domain, existing);\n\t\t\t\t\tpendingItemCategory = { domain, nicename };\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tparser.ontext = (text) => {\n\t\t\tcurrentText += text;\n\t\t};\n\n\t\tparser.oncdata = (cdata) => {\n\t\t\tcurrentText += cdata;\n\t\t};\n\n\t\tparser.onclosetag = (tagName) => {\n\t\t\tconst tag = tagName.toLowerCase();\n\t\t\tconst text = currentText.trim();\n\n\t\t\t// Site metadata\n\t\t\tif (currentPath.length === 2 && currentPath[0] === \"rss\") {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"title\":\n\t\t\t\t\t\tdata.site.title = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"link\":\n\t\t\t\t\t\tdata.site.link = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"description\":\n\t\t\t\t\t\tdata.site.description = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"language\":\n\t\t\t\t\t\tdata.site.language = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:base_site_url\":\n\t\t\t\t\t\tdata.site.baseSiteUrl = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:base_blog_url\":\n\t\t\t\t\t\tdata.site.baseBlogUrl = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Item (post/page/attachment) parsing\n\t\t\tif (currentItem) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"title\":\n\t\t\t\t\t\tcurrentItem.title = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"link\":\n\t\t\t\t\t\tcurrentItem.link = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"pubdate\":\n\t\t\t\t\t\tcurrentItem.pubDate = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"dc:creator\":\n\t\t\t\t\t\tcurrentItem.creator = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"guid\":\n\t\t\t\t\t\tcurrentItem.guid = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"description\":\n\t\t\t\t\t\tcurrentItem.description = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"content:encoded\":\n\t\t\t\t\t\tcurrentItem.content = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"excerpt:encoded\":\n\t\t\t\t\t\tcurrentItem.excerpt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_id\":\n\t\t\t\t\t\tcurrentItem.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_date\":\n\t\t\t\t\t\tcurrentItem.postDate = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_date_gmt\":\n\t\t\t\t\t\tcurrentItem.postDateGmt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_modified\":\n\t\t\t\t\t\tcurrentItem.postModified = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_modified_gmt\":\n\t\t\t\t\t\tcurrentItem.postModifiedGmt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:comment_status\":\n\t\t\t\t\t\tcurrentItem.commentStatus = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:ping_status\":\n\t\t\t\t\t\tcurrentItem.pingStatus = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_name\":\n\t\t\t\t\t\tcurrentItem.postName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:status\":\n\t\t\t\t\t\tcurrentItem.status = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_parent\":\n\t\t\t\t\t\tcurrentItem.postParent = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:menu_order\":\n\t\t\t\t\t\tcurrentItem.menuOrder = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_type\":\n\t\t\t\t\t\tcurrentItem.postType = text;\n\t\t\t\t\t\t// If it's an attachment, convert to attachment type\n\t\t\t\t\t\tif (text === \"attachment\") {\n\t\t\t\t\t\t\tcurrentAttachment = {\n\t\t\t\t\t\t\t\tid: currentItem.id,\n\t\t\t\t\t\t\t\ttitle: currentItem.title,\n\t\t\t\t\t\t\t\turl: currentItem.link,\n\t\t\t\t\t\t\t\tpostDate: currentItem.postDate,\n\t\t\t\t\t\t\t\tmeta: new Map(),\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_password\":\n\t\t\t\t\t\tcurrentItem.postPassword = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:is_sticky\":\n\t\t\t\t\t\tcurrentItem.isSticky = text === \"1\";\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:attachment_url\":\n\t\t\t\t\t\tif (currentAttachment) {\n\t\t\t\t\t\t\tcurrentAttachment.url = text;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:meta_key\":\n\t\t\t\t\t\tcurrentMetaKey = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:meta_value\":\n\t\t\t\t\t\tif (currentMetaKey && currentItem.meta) {\n\t\t\t\t\t\t\tcurrentItem.meta.set(currentMetaKey, text);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"category\":\n\t\t\t\t\t\t// Per-item category text body = human label. See\n\t\t\t\t\t\t// streaming-parser counterpart for rationale.\n\t\t\t\t\t\tif (pendingItemCategory && text) {\n\t\t\t\t\t\t\tcaptureItemCategoryLabel(currentItem, pendingItemCategory, text);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpendingItemCategory = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"item\":\n\t\t\t\t\t\t// End of item - categorize and store\n\t\t\t\t\t\tif (currentAttachment) {\n\t\t\t\t\t\t\tdata.attachments.push(currentAttachment);\n\t\t\t\t\t\t\tcurrentAttachment = null;\n\t\t\t\t\t\t} else if (currentItem.postType === \"nav_menu_item\") {\n\t\t\t\t\t\t\t// Track nav_menu_item posts for post-processing into menus\n\t\t\t\t\t\t\tnavMenuItemPosts.push(currentItem);\n\t\t\t\t\t\t\tdata.posts.push(currentItem);\n\t\t\t\t\t\t} else if (currentItem.postType !== \"attachment\") {\n\t\t\t\t\t\t\t// Promote multilingual plugin metadata before storing.\n\t\t\t\t\t\t\t// All postmeta and per-item categories are parsed by the time\n\t\t\t\t\t\t\t// the closing </item> tag fires, so it's safe to inspect them.\n\t\t\t\t\t\t\tpromoteI18nMetadata(currentItem);\n\t\t\t\t\t\t\tdata.posts.push(currentItem);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentItem = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Category parsing\n\t\t\tif (currentCategory) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentCategory.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_nicename\":\n\t\t\t\t\t\tcurrentCategory.nicename = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:cat_name\":\n\t\t\t\t\t\tcurrentCategory.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_parent\":\n\t\t\t\t\t\tcurrentCategory.parent = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_description\":\n\t\t\t\t\t\tcurrentCategory.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category\":\n\t\t\t\t\t\tif (currentCategory.name) {\n\t\t\t\t\t\t\tdata.categories.push(currentCategory);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentCategory = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Tag parsing\n\t\t\tif (currentTag) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentTag.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_slug\":\n\t\t\t\t\t\tcurrentTag.slug = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_name\":\n\t\t\t\t\t\tcurrentTag.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_description\":\n\t\t\t\t\t\tcurrentTag.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag\":\n\t\t\t\t\t\tif (currentTag.name) {\n\t\t\t\t\t\t\tdata.tags.push(currentTag);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentTag = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Author parsing\n\t\t\tif (currentAuthor) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:author_id\":\n\t\t\t\t\t\tcurrentAuthor.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_login\":\n\t\t\t\t\t\tcurrentAuthor.login = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_email\":\n\t\t\t\t\t\tcurrentAuthor.email = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_display_name\":\n\t\t\t\t\t\tcurrentAuthor.displayName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_first_name\":\n\t\t\t\t\t\tcurrentAuthor.firstName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_last_name\":\n\t\t\t\t\t\tcurrentAuthor.lastName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author\":\n\t\t\t\t\t\tif (currentAuthor.login) {\n\t\t\t\t\t\t\tdata.authors.push(currentAuthor);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentAuthor = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Generic term parsing (wp:term elements - custom taxonomies, nav_menu, etc.)\n\t\t\tif (currentTerm) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentTerm.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_taxonomy\":\n\t\t\t\t\t\tcurrentTerm.taxonomy = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_slug\":\n\t\t\t\t\t\tcurrentTerm.slug = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_name\":\n\t\t\t\t\t\tcurrentTerm.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_parent\":\n\t\t\t\t\t\tcurrentTerm.parent = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_description\":\n\t\t\t\t\t\tcurrentTerm.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term\":\n\t\t\t\t\t\tif (isCompleteWxrTerm(currentTerm)) {\n\t\t\t\t\t\t\tdata.terms.push(currentTerm);\n\t\t\t\t\t\t\t// Track nav_menu terms for building menus\n\t\t\t\t\t\t\tif (currentTerm.taxonomy === \"nav_menu\") {\n\t\t\t\t\t\t\t\tmenuTermsBySlug.set(currentTerm.slug, currentTerm.id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentTerm = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcurrentPath.pop();\n\t\t\tcurrentText = \"\";\n\t\t};\n\n\t\tparser.onerror = (err) => {\n\t\t\treject(new Error(`XML parsing error: ${err.message}`));\n\t\t};\n\n\t\tparser.onend = () => {\n\t\t\t// Post-process nav_menu_item posts into structured menus\n\t\t\tdata.navMenus = buildNavMenus(navMenuItemPosts, menuTermsBySlug);\n\t\t\tresolve(data);\n\t\t};\n\n\t\t// Parse the string (non-streaming)\n\t\tparser.write(xml).close();\n\t});\n}\n\n/**\n * Build structured navigation menus from nav_menu_item posts\n */\nfunction buildNavMenus(\n\tnavMenuItemPosts: WxrPost[],\n\tmenuTermsBySlug: Map<string, number>,\n): WxrNavMenu[] {\n\t// Group menu items by menu slug\n\tconst menuItemsByMenu = new Map<string, WxrPost[]>();\n\n\tfor (const post of navMenuItemPosts) {\n\t\t// Get the nav_menu taxonomy assignment to find which menu this item belongs to\n\t\tconst navMenuSlugs = post.customTaxonomies?.get(\"nav_menu\");\n\t\tif (!navMenuSlugs || navMenuSlugs.length === 0) continue;\n\n\t\tconst menuSlug = navMenuSlugs[0];\n\t\tif (!menuSlug) continue;\n\n\t\tconst items = menuItemsByMenu.get(menuSlug) || [];\n\t\titems.push(post);\n\t\tmenuItemsByMenu.set(menuSlug, items);\n\t}\n\n\t// Build structured menus\n\tconst menus: WxrNavMenu[] = [];\n\n\tfor (const [menuSlug, posts] of menuItemsByMenu) {\n\t\tconst menuId = menuTermsBySlug.get(menuSlug) || 0;\n\n\t\t// Convert posts to menu items\n\t\tconst items: WxrNavMenuItem[] = posts.map((post) => {\n\t\t\tconst meta = post.meta;\n\t\t\tconst menuItemTypeRaw = meta.get(\"_menu_item_type\") || \"custom\";\n\t\t\tconst menuItemType: WxrNavMenuItem[\"type\"] =\n\t\t\t\tmenuItemTypeRaw === \"post_type\" || menuItemTypeRaw === \"taxonomy\"\n\t\t\t\t\t? menuItemTypeRaw\n\t\t\t\t\t: \"custom\";\n\t\t\tconst objectType = meta.get(\"_menu_item_object\");\n\t\t\tconst objectIdStr = meta.get(\"_menu_item_object_id\");\n\t\t\tconst url = meta.get(\"_menu_item_url\");\n\t\t\tconst parentIdStr = meta.get(\"_menu_item_menu_item_parent\");\n\t\t\tconst target = meta.get(\"_menu_item_target\");\n\t\t\tconst classesStr = meta.get(\"_menu_item_classes\");\n\n\t\t\t// Parse classes (stored as serialized PHP array)\n\t\t\tlet classes: string | undefined;\n\t\t\tif (classesStr) {\n\t\t\t\t// Simple extraction of class names from serialized PHP\n\t\t\t\tconst matches = classesStr.match(PHP_SERIALIZED_STRING_PATTERN);\n\t\t\t\tif (matches) {\n\t\t\t\t\tclasses = matches\n\t\t\t\t\t\t.map((m) => m.match(PHP_SERIALIZED_STRING_MATCH_PATTERN)?.[1])\n\t\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t\t.join(\" \");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tid: post.id || 0,\n\t\t\t\tmenuId,\n\t\t\t\tparentId: parentIdStr ? parseInt(parentIdStr, 10) || undefined : undefined,\n\t\t\t\tsortOrder: post.menuOrder || 0,\n\t\t\t\ttype: menuItemType,\n\t\t\t\tobjectType: objectType || undefined,\n\t\t\t\tobjectId: objectIdStr ? parseInt(objectIdStr, 10) : undefined,\n\t\t\t\turl: url || undefined,\n\t\t\t\ttitle: post.title || \"\",\n\t\t\t\ttarget: target || undefined,\n\t\t\t\tclasses: classes || undefined,\n\t\t\t};\n\t\t});\n\n\t\t// Sort items by menu_order\n\t\titems.sort((a, b) => a.sortOrder - b.sortOrder);\n\n\t\t// Find the menu name from the terms\n\t\t// For now, use the slug as both name and label; we could enhance this\n\t\t// by looking up the actual term name from data.terms\n\t\tmenus.push({\n\t\t\tid: menuId,\n\t\t\tname: menuSlug,\n\t\t\tlabel: menuSlug, // Will be enhanced when we have term data\n\t\t\titems,\n\t\t});\n\t}\n\n\treturn menus;\n}\n","/**\n * Menu import functions\n *\n * Import navigation menus from WordPress WXR exports or plugin API.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { WxrNavMenu, WxrNavMenuItem } from \"../cli/wxr/parser.js\";\nimport type { Database } from \"../database/types.js\";\nimport type { MenuItemType } from \"../menus/types.js\";\n\n/**\n * Result of menu import operation\n */\nexport interface MenuImportResult {\n\t/** Number of menus created */\n\tmenusCreated: number;\n\t/** Number of menu items created */\n\titemsCreated: number;\n\t/** Mapping from WP menu slug to EmDash menu ID */\n\tmenuIdMap: Map<string, string>;\n\t/** Errors encountered during import */\n\terrors: Array<{ menu: string; error: string }>;\n}\n\n/**\n * Plugin API menu format (matches /emdash/v1/menus response)\n */\nexport interface PluginMenu {\n\tid: number;\n\tname: string; // slug\n\tlabel: string;\n\titems: PluginMenuItem[];\n}\n\nexport interface PluginMenuItem {\n\tid: number;\n\tparent_id: number | null;\n\tsort_order: number;\n\ttype: \"custom\" | \"post_type\" | \"taxonomy\";\n\tobject: string | null; // 'page', 'post', 'category'\n\tobject_id: number | null;\n\turl: string;\n\ttitle: string;\n\ttarget: string | null;\n\tclasses: string | null;\n}\n\n/**\n * Import navigation menus from WXR export\n *\n * @param menus - Parsed navigation menus from WXR\n * @param db - Database connection\n * @param contentIdMap - Map from WP post ID to EmDash content ID (for resolving references)\n * @returns Import result with counts and ID mapping\n */\nexport async function importMenusFromWxr(\n\tmenus: WxrNavMenu[],\n\tdb: Kysely<Database>,\n\tcontentIdMap: Map<number, string>,\n): Promise<MenuImportResult> {\n\tconst result: MenuImportResult = {\n\t\tmenusCreated: 0,\n\t\titemsCreated: 0,\n\t\tmenuIdMap: new Map(),\n\t\terrors: [],\n\t};\n\n\tfor (const menu of menus) {\n\t\ttry {\n\t\t\t// Check if menu already exists\n\t\t\tconst existing = await db\n\t\t\t\t.selectFrom(\"_emdash_menus\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"name\", \"=\", menu.name)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (existing) {\n\t\t\t\tresult.menuIdMap.set(menu.name, existing.id);\n\t\t\t\tcontinue; // Skip existing menus\n\t\t\t}\n\n\t\t\t// Create the menu\n\t\t\tconst menuId = ulid();\n\t\t\tawait db\n\t\t\t\t.insertInto(\"_emdash_menus\")\n\t\t\t\t.values({\n\t\t\t\t\tid: menuId,\n\t\t\t\t\tname: menu.name,\n\t\t\t\t\tlabel: menu.label,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\tresult.menusCreated++;\n\t\t\tresult.menuIdMap.set(menu.name, menuId);\n\n\t\t\t// Import menu items\n\t\t\tconst itemsCreated = await importWxrMenuItems(menu.items, menuId, db, contentIdMap);\n\t\t\tresult.itemsCreated += itemsCreated;\n\t\t} catch (error) {\n\t\t\tresult.errors.push({\n\t\t\t\tmenu: menu.name,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Import navigation menus from Plugin API\n *\n * @param menus - Menus from plugin API\n * @param db - Database connection\n * @param contentIdMap - Map from WP post ID to EmDash content ID\n * @returns Import result with counts and ID mapping\n */\nexport async function importMenusFromPlugin(\n\tmenus: PluginMenu[],\n\tdb: Kysely<Database>,\n\tcontentIdMap: Map<number, string>,\n): Promise<MenuImportResult> {\n\tconst result: MenuImportResult = {\n\t\tmenusCreated: 0,\n\t\titemsCreated: 0,\n\t\tmenuIdMap: new Map(),\n\t\terrors: [],\n\t};\n\n\tfor (const menu of menus) {\n\t\ttry {\n\t\t\t// Check if menu already exists\n\t\t\tconst existing = await db\n\t\t\t\t.selectFrom(\"_emdash_menus\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"name\", \"=\", menu.name)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (existing) {\n\t\t\t\tresult.menuIdMap.set(menu.name, existing.id);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Create the menu\n\t\t\tconst menuId = ulid();\n\t\t\tawait db\n\t\t\t\t.insertInto(\"_emdash_menus\")\n\t\t\t\t.values({\n\t\t\t\t\tid: menuId,\n\t\t\t\t\tname: menu.name,\n\t\t\t\t\tlabel: menu.label,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\tresult.menusCreated++;\n\t\t\tresult.menuIdMap.set(menu.name, menuId);\n\n\t\t\t// Import menu items\n\t\t\tconst itemsCreated = await importPluginMenuItems(menu.items, menuId, db, contentIdMap);\n\t\t\tresult.itemsCreated += itemsCreated;\n\t\t} catch (error) {\n\t\t\tresult.errors.push({\n\t\t\t\tmenu: menu.name,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Import menu items from WXR format\n */\nasync function importWxrMenuItems(\n\titems: WxrNavMenuItem[],\n\tmenuId: string,\n\tdb: Kysely<Database>,\n\tcontentIdMap: Map<number, string>,\n): Promise<number> {\n\t// Build a map of WP menu item IDs to EmDash IDs for parent resolution\n\tconst itemIdMap = new Map<number, string>();\n\tlet count = 0;\n\n\t// Sort items by sort order to maintain hierarchy\n\tconst sortedItems = items.toSorted((a, b) => a.sortOrder - b.sortOrder);\n\n\t// First pass: create all items with temporary parent IDs\n\tfor (const item of sortedItems) {\n\t\tconst itemId = ulid();\n\t\titemIdMap.set(item.id, itemId);\n\n\t\tconst { type, collection, referenceId, customUrl } = mapWxrMenuItem(item, contentIdMap);\n\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_menu_items\")\n\t\t\t.values({\n\t\t\t\tid: itemId,\n\t\t\t\tmenu_id: menuId,\n\t\t\t\tparent_id: null, // Will be set in second pass\n\t\t\t\tsort_order: item.sortOrder,\n\t\t\t\ttype,\n\t\t\t\treference_collection: collection,\n\t\t\t\treference_id: referenceId,\n\t\t\t\tcustom_url: customUrl,\n\t\t\t\tlabel: item.title,\n\t\t\t\ttitle_attr: null,\n\t\t\t\ttarget: item.target || null,\n\t\t\t\tcss_classes: item.classes || null,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tcount++;\n\t}\n\n\t// Second pass: update parent IDs\n\tfor (const item of sortedItems) {\n\t\tif (item.parentId) {\n\t\t\tconst itemId = itemIdMap.get(item.id);\n\t\t\tconst parentId = itemIdMap.get(item.parentId);\n\n\t\t\tif (itemId && parentId) {\n\t\t\t\tawait db\n\t\t\t\t\t.updateTable(\"_emdash_menu_items\")\n\t\t\t\t\t.set({ parent_id: parentId })\n\t\t\t\t\t.where(\"id\", \"=\", itemId)\n\t\t\t\t\t.execute();\n\t\t\t}\n\t\t}\n\t}\n\n\treturn count;\n}\n\n/**\n * Import menu items from Plugin API format\n */\nasync function importPluginMenuItems(\n\titems: PluginMenuItem[],\n\tmenuId: string,\n\tdb: Kysely<Database>,\n\tcontentIdMap: Map<number, string>,\n): Promise<number> {\n\tconst itemIdMap = new Map<number, string>();\n\tlet count = 0;\n\n\tconst sortedItems = items.toSorted((a, b) => a.sort_order - b.sort_order);\n\n\tfor (const item of sortedItems) {\n\t\tconst itemId = ulid();\n\t\titemIdMap.set(item.id, itemId);\n\n\t\tconst { type, collection, referenceId, customUrl } = mapPluginMenuItem(item, contentIdMap);\n\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_menu_items\")\n\t\t\t.values({\n\t\t\t\tid: itemId,\n\t\t\t\tmenu_id: menuId,\n\t\t\t\tparent_id: null,\n\t\t\t\tsort_order: item.sort_order,\n\t\t\t\ttype,\n\t\t\t\treference_collection: collection,\n\t\t\t\treference_id: referenceId,\n\t\t\t\tcustom_url: customUrl,\n\t\t\t\tlabel: item.title,\n\t\t\t\ttitle_attr: null,\n\t\t\t\ttarget: item.target || null,\n\t\t\t\tcss_classes: item.classes || null,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tcount++;\n\t}\n\n\t// Second pass: update parent IDs\n\tfor (const item of sortedItems) {\n\t\tif (item.parent_id) {\n\t\t\tconst itemId = itemIdMap.get(item.id);\n\t\t\tconst parentId = itemIdMap.get(item.parent_id);\n\n\t\t\tif (itemId && parentId) {\n\t\t\t\tawait db\n\t\t\t\t\t.updateTable(\"_emdash_menu_items\")\n\t\t\t\t\t.set({ parent_id: parentId })\n\t\t\t\t\t.where(\"id\", \"=\", itemId)\n\t\t\t\t\t.execute();\n\t\t\t}\n\t\t}\n\t}\n\n\treturn count;\n}\n\n/**\n * Map WXR menu item to EmDash format\n */\nfunction mapWxrMenuItem(\n\titem: WxrNavMenuItem,\n\tcontentIdMap: Map<number, string>,\n): {\n\ttype: MenuItemType;\n\tcollection: string | null;\n\treferenceId: string | null;\n\tcustomUrl: string | null;\n} {\n\tswitch (item.type) {\n\t\tcase \"custom\":\n\t\t\treturn {\n\t\t\t\ttype: \"custom\",\n\t\t\t\tcollection: null,\n\t\t\t\treferenceId: null,\n\t\t\t\tcustomUrl: item.url || \"#\",\n\t\t\t};\n\n\t\tcase \"post_type\": {\n\t\t\t// Map WordPress object type to collection\n\t\t\tconst collection = mapObjectToCollection(item.objectType);\n\t\t\tconst referenceId = item.objectId ? contentIdMap.get(item.objectId) || null : null;\n\n\t\t\t// If we can't resolve the reference, fall back to custom URL\n\t\t\tif (!referenceId && item.url) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"custom\",\n\t\t\t\t\tcollection: null,\n\t\t\t\t\treferenceId: null,\n\t\t\t\t\tcustomUrl: item.url,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: collection === \"pages\" ? \"page\" : \"post\",\n\t\t\t\tcollection,\n\t\t\t\treferenceId,\n\t\t\t\tcustomUrl: null,\n\t\t\t};\n\t\t}\n\n\t\tcase \"taxonomy\":\n\t\t\t// For taxonomies, we need taxonomy support in menus\n\t\t\t// Fall back to custom URL for now\n\t\t\treturn {\n\t\t\t\ttype: \"custom\",\n\t\t\t\tcollection: null,\n\t\t\t\treferenceId: null,\n\t\t\t\tcustomUrl: item.url || \"#\",\n\t\t\t};\n\n\t\tdefault:\n\t\t\treturn {\n\t\t\t\ttype: \"custom\",\n\t\t\t\tcollection: null,\n\t\t\t\treferenceId: null,\n\t\t\t\tcustomUrl: item.url || \"#\",\n\t\t\t};\n\t}\n}\n\n/**\n * Map Plugin menu item to EmDash format\n */\nfunction mapPluginMenuItem(\n\titem: PluginMenuItem,\n\tcontentIdMap: Map<number, string>,\n): {\n\ttype: MenuItemType;\n\tcollection: string | null;\n\treferenceId: string | null;\n\tcustomUrl: string | null;\n} {\n\tswitch (item.type) {\n\t\tcase \"custom\":\n\t\t\treturn {\n\t\t\t\ttype: \"custom\",\n\t\t\t\tcollection: null,\n\t\t\t\treferenceId: null,\n\t\t\t\tcustomUrl: item.url || \"#\",\n\t\t\t};\n\n\t\tcase \"post_type\": {\n\t\t\tconst collection = mapObjectToCollection(item.object);\n\t\t\tconst referenceId = item.object_id ? contentIdMap.get(item.object_id) || null : null;\n\n\t\t\tif (!referenceId && item.url) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"custom\",\n\t\t\t\t\tcollection: null,\n\t\t\t\t\treferenceId: null,\n\t\t\t\t\tcustomUrl: item.url,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: collection === \"pages\" ? \"page\" : \"post\",\n\t\t\t\tcollection,\n\t\t\t\treferenceId,\n\t\t\t\tcustomUrl: null,\n\t\t\t};\n\t\t}\n\n\t\tcase \"taxonomy\":\n\t\t\treturn {\n\t\t\t\ttype: \"custom\",\n\t\t\t\tcollection: null,\n\t\t\t\treferenceId: null,\n\t\t\t\tcustomUrl: item.url || \"#\",\n\t\t\t};\n\n\t\tdefault:\n\t\t\treturn {\n\t\t\t\ttype: \"custom\",\n\t\t\t\tcollection: null,\n\t\t\t\treferenceId: null,\n\t\t\t\tcustomUrl: item.url || \"#\",\n\t\t\t};\n\t}\n}\n\n/**\n * Map WordPress object type to EmDash collection name\n */\nfunction mapObjectToCollection(objectType: string | undefined | null): string {\n\tif (!objectType) return \"posts\";\n\n\tconst mapping: Record<string, string> = {\n\t\tpost: \"posts\",\n\t\tpage: \"pages\",\n\t\tproduct: \"products\",\n\t\tportfolio: \"portfolio\",\n\t};\n\n\treturn mapping[objectType] || objectType;\n}\n","/**\n * Sections import functions\n *\n * Import reusable blocks from WordPress WXR exports as EmDash sections.\n */\n\nimport type { PortableTextBlock } from \"@premium-cms/gutenberg-to-portable-text\";\nimport { gutenbergToPortableText } from \"@premium-cms/gutenberg-to-portable-text\";\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { WxrPost } from \"../cli/wxr/parser.js\";\nimport type { Database } from \"../database/types.js\";\nimport { slugify } from \"../utils/slugify.js\";\n\n/**\n * Result of sections import operation\n */\nexport interface SectionsImportResult {\n\t/** Number of sections created */\n\tsectionsCreated: number;\n\t/** Number of sections skipped (already exist) */\n\tsectionsSkipped: number;\n\t/** Errors encountered during import */\n\terrors: Array<{ title: string; error: string }>;\n}\n\n/**\n * Import reusable blocks (wp_block post type) from WXR as sections\n *\n * @param posts - All posts from WXR (will filter to wp_block)\n * @param db - Database connection\n * @returns Import result with counts\n */\nexport async function importReusableBlocksAsSections(\n\tposts: WxrPost[],\n\tdb: Kysely<Database>,\n): Promise<SectionsImportResult> {\n\tconst result: SectionsImportResult = {\n\t\tsectionsCreated: 0,\n\t\tsectionsSkipped: 0,\n\t\terrors: [],\n\t};\n\n\t// Filter to only wp_block posts\n\tconst reusableBlocks = posts.filter((post) => post.postType === \"wp_block\");\n\n\tif (reusableBlocks.length === 0) {\n\t\treturn result;\n\t}\n\n\tfor (const block of reusableBlocks) {\n\t\ttry {\n\t\t\tconst slug = block.postName || slugify(block.title || `block-${block.id || Date.now()}`);\n\n\t\t\t// Check if section already exists\n\t\t\tconst existing = await db\n\t\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (existing) {\n\t\t\t\tresult.sectionsSkipped++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Convert Gutenberg content to Portable Text\n\t\t\tconst content: PortableTextBlock[] = block.content\n\t\t\t\t? gutenbergToPortableText(block.content)\n\t\t\t\t: [];\n\n\t\t\tconst id = ulid();\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tawait db\n\t\t\t\t.insertInto(\"_emdash_sections\")\n\t\t\t\t.values({\n\t\t\t\t\tid,\n\t\t\t\t\tslug,\n\t\t\t\t\ttitle: block.title || \"Untitled Block\",\n\t\t\t\t\tdescription: null,\n\t\t\t\t\tkeywords: null,\n\t\t\t\t\tcontent: JSON.stringify(content),\n\t\t\t\t\tpreview_media_id: null,\n\t\t\t\t\tsource: \"import\",\n\t\t\t\t\ttheme_id: null,\n\t\t\t\t\tcreated_at: now,\n\t\t\t\t\tupdated_at: now,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\tresult.sectionsCreated++;\n\t\t} catch (error) {\n\t\t\tresult.errors.push({\n\t\t\t\ttitle: block.title || \"Untitled Block\",\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t}\n\n\treturn result;\n}\n","/**\n * Comment import functions\n *\n * Import comments from the WordPress plugin API into EmDash's native\n * comments table, preserving authors, dates, threading, and status.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database } from \"../database/types.js\";\nimport { invalidateCommentObjectCache } from \"../object-cache/index.js\";\n\n/**\n * Plugin API comment format (matches /emdash/v1/comments items)\n */\nexport interface PluginComment {\n\tid: number;\n\tpost_id: number;\n\tparent_id: number | null;\n\tauthor_name: string;\n\tauthor_email: string;\n\t/** Plain-text body (the plugin strips HTML) */\n\tbody: string;\n\t/** ISO 8601 UTC timestamp */\n\tdate_gmt: string;\n\tstatus: \"approved\" | \"pending\";\n}\n\n/**\n * Result of comment import operation\n */\nexport interface CommentsImportResult {\n\t/** Number of comments created */\n\timported: number;\n\t/** Comments skipped (unresolvable post reference or already imported) */\n\tskipped: number;\n\t/** Errors encountered */\n\terrors: Array<{ comment: string; error: string }>;\n}\n\n/**\n * Import comments from the plugin API.\n *\n * Preserves original timestamps and threading. Comments whose post was\n * not imported (no entry in `contentIdMap`) are skipped. Re-running the\n * import is idempotent: a comment with the same post, author email, and\n * timestamp is not created twice.\n *\n * @param comments - Comments from the plugin API (all pages, flat)\n * @param db - Database connection\n * @param contentIdMap - WP post ID -> EmDash content ID\n * @param collectionMap - WP post ID -> EmDash collection slug\n * @param rootIds - Optional pre-seeded WP-comment-ID -> EmDash-root-ID map.\n *   The chunked import passes the map accumulated from earlier pages so a\n *   reply in page N can thread onto a parent imported in page N-1; the\n *   function adds this page's entries to it.\n */\nexport async function importCommentsFromPlugin(\n\tcomments: PluginComment[],\n\tdb: Kysely<Database>,\n\tcontentIdMap: Map<number, string>,\n\tcollectionMap: Map<number, string>,\n\trootIds?: Map<number, string>,\n): Promise<CommentsImportResult> {\n\tconst result: CommentsImportResult = {\n\t\timported: 0,\n\t\tskipped: 0,\n\t\terrors: [],\n\t};\n\n\t// WP comment ID -> EmDash comment ID, for parent threading\n\tconst commentIdMap = new Map<number, string>();\n\n\t// WP comment ID -> EmDash ID of its top-level ancestor. EmDash threads\n\t// are one level deep (assembleThreads nests replies under roots only),\n\t// so deeper WP threads are flattened onto their root comment.\n\tconst rootIdMap = rootIds ?? new Map<number, string>();\n\n\t// Parents must exist before children reference them; WP comment IDs\n\t// are chronological, so ID order guarantees parents come first.\n\tconst sorted = comments.toSorted((a, b) => a.id - b.id);\n\n\tfor (const comment of sorted) {\n\t\tconst label = `${comment.author_name || \"Anonymous\"} (${comment.date_gmt})`;\n\t\ttry {\n\t\t\tconst contentId = contentIdMap.get(comment.post_id);\n\t\t\tconst collection = collectionMap.get(comment.post_id);\n\t\t\tif (!contentId || !collection) {\n\t\t\t\tresult.skipped++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst parsed = new Date(comment.date_gmt);\n\t\t\tconst createdAt = Number.isNaN(parsed.getTime())\n\t\t\t\t? new Date().toISOString()\n\t\t\t\t: parsed.toISOString();\n\n\t\t\t// Idempotency: same post + author + timestamp + body = already\n\t\t\t// imported (body included so same-second comments from one author\n\t\t\t// don't collide). ponytail: one SELECT per comment — fine at\n\t\t\t// import scale; batch with WHERE IN for six-figure comment counts.\n\t\t\tconst existing = await db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t\t.where(\"author_email\", \"=\", comment.author_email)\n\t\t\t\t.where(\"created_at\", \"=\", createdAt)\n\t\t\t\t.where(\"body\", \"=\", comment.body)\n\t\t\t\t.executeTakeFirst();\n\t\t\tconst parentId =\n\t\t\t\tcomment.parent_id !== null ? (rootIdMap.get(comment.parent_id) ?? null) : null;\n\n\t\t\tif (existing) {\n\t\t\t\tcommentIdMap.set(comment.id, existing.id);\n\t\t\t\trootIdMap.set(comment.id, parentId ?? existing.id);\n\t\t\t\tresult.skipped++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst id = ulid();\n\n\t\t\tawait db\n\t\t\t\t.insertInto(\"_emdash_comments\")\n\t\t\t\t.values({\n\t\t\t\t\tid,\n\t\t\t\t\tcollection,\n\t\t\t\t\tcontent_id: contentId,\n\t\t\t\t\tparent_id: parentId,\n\t\t\t\t\tauthor_name: comment.author_name || \"Anonymous\",\n\t\t\t\t\tauthor_email: comment.author_email,\n\t\t\t\t\tauthor_user_id: null,\n\t\t\t\t\tbody: comment.body,\n\t\t\t\t\tstatus: comment.status,\n\t\t\t\t\tip_hash: null,\n\t\t\t\t\tuser_agent: null,\n\t\t\t\t\tmoderation_metadata: null,\n\t\t\t\t\tcreated_at: createdAt,\n\t\t\t\t\tupdated_at: createdAt,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\tcommentIdMap.set(comment.id, id);\n\t\t\trootIdMap.set(comment.id, parentId ?? id);\n\t\t\tresult.imported++;\n\t\t} catch (error) {\n\t\t\tresult.errors.push({\n\t\t\t\tcomment: label,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t}\n\n\tif (result.imported > 0) {\n\t\tinvalidateCommentObjectCache();\n\t}\n\n\treturn result;\n}\n","/**\n * Site settings import functions\n *\n * Import site settings from WordPress (title, tagline, logo, favicon)\n * into EmDash's site settings (the `site:*` options read by\n * `getSiteSettings()` and the templates).\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport type { Database } from \"../database/types.js\";\nimport { invalidateSiteSettingsCache } from \"../settings/index.js\";\nimport type { SiteSettings } from \"../settings/types.js\";\n\n/** Options key prefix used by the site settings module (`settings/index.ts`). */\nconst SETTINGS_PREFIX = \"site:\";\n\n/**\n * Site settings analysis from import source\n */\nexport interface SiteSettingsAnalysis {\n\t/** Site title */\n\ttitle?: string;\n\t/** Site tagline/description */\n\ttagline?: string;\n\t/** Custom logo (source URL + WP attachment ID) */\n\tlogo?: { url: string; id?: number };\n\t/** Favicon/site icon (source URL + WP attachment ID) */\n\tfavicon?: { url: string; id?: number };\n}\n\n/**\n * Result of site settings import\n */\nexport interface SettingsImportResult {\n\t/** Settings that were applied */\n\tapplied: string[];\n\t/** Settings that were skipped (already set) */\n\tskipped: string[];\n\t/** Errors encountered */\n\terrors: Array<{ setting: string; error: string }>;\n}\n\n/**\n * Resolved EmDash media IDs for the logo/favicon source URLs.\n * The caller side-loads the files (they need storage access) and\n * passes the resulting media IDs here.\n */\nexport interface SettingsMediaIds {\n\tlogoMediaId?: string;\n\tfaviconMediaId?: string;\n}\n\n/**\n * Import site settings from analysis into EmDash site settings.\n *\n * Writes the `site:*` options consumed by `getSiteSettings()` and\n * invalidates the settings cache. Logo/favicon are only applied when\n * the caller resolved them to EmDash media IDs.\n *\n * @param settings - Site settings analysis\n * @param db - Database connection\n * @param overwrite - Whether to overwrite existing settings (a fresh\n *   site's seed already sets title/tagline, so migrations pass true)\n * @param media - Resolved media IDs for logo/favicon\n */\nexport async function importSiteSettings(\n\tsettings: SiteSettingsAnalysis,\n\tdb: Kysely<Database>,\n\toverwrite = false,\n\tmedia: SettingsMediaIds = {},\n): Promise<SettingsImportResult> {\n\tconst result: SettingsImportResult = {\n\t\tapplied: [],\n\t\tskipped: [],\n\t\terrors: [],\n\t};\n\n\tconst updates: Array<{ setting: keyof SiteSettings; value: unknown }> = [];\n\tif (settings.title) {\n\t\tupdates.push({ setting: \"title\", value: settings.title });\n\t}\n\tif (settings.tagline) {\n\t\tupdates.push({ setting: \"tagline\", value: settings.tagline });\n\t}\n\tif (media.logoMediaId) {\n\t\tupdates.push({ setting: \"logo\", value: { mediaId: media.logoMediaId } });\n\t}\n\tif (media.faviconMediaId) {\n\t\tupdates.push({ setting: \"favicon\", value: { mediaId: media.faviconMediaId } });\n\t}\n\n\tif (updates.length === 0) {\n\t\treturn result;\n\t}\n\n\tconst options = new OptionsRepository(db);\n\ttry {\n\t\tfor (const { setting, value } of updates) {\n\t\t\ttry {\n\t\t\t\tconst key = `${SETTINGS_PREFIX}${setting}`;\n\t\t\t\tif (!overwrite) {\n\t\t\t\t\tconst existing = await options.get(key);\n\t\t\t\t\tif (existing !== null) {\n\t\t\t\t\t\tresult.skipped.push(setting);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tawait options.set(key, value);\n\t\t\t\tresult.applied.push(setting);\n\t\t\t} catch (error) {\n\t\t\t\tresult.errors.push({\n\t\t\t\t\tsetting,\n\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tif (result.applied.length > 0) {\n\t\t\tinvalidateSiteSettingsCache();\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Parse site settings from WordPress plugin options response\n */\nexport function parseSiteSettingsFromPlugin(\n\toptions: Record<string, unknown>,\n): SiteSettingsAnalysis {\n\tconst settings: SiteSettingsAnalysis = {};\n\n\tif (typeof options.blogname === \"string\" && options.blogname.trim() !== \"\") {\n\t\tsettings.title = options.blogname;\n\t}\n\tif (typeof options.blogdescription === \"string\" && options.blogdescription.trim() !== \"\") {\n\t\tsettings.tagline = options.blogdescription;\n\t}\n\n\tif (typeof options.custom_logo_url === \"string\" && options.custom_logo_url !== \"\") {\n\t\tsettings.logo = {\n\t\t\turl: options.custom_logo_url,\n\t\t\tid: typeof options.custom_logo === \"number\" ? options.custom_logo : undefined,\n\t\t};\n\t}\n\tif (typeof options.site_icon_url === \"string\" && options.site_icon_url !== \"\") {\n\t\tsettings.favicon = {\n\t\t\turl: options.site_icon_url,\n\t\t\tid: typeof options.site_icon === \"number\" ? options.site_icon : undefined,\n\t\t};\n\t}\n\n\treturn settings;\n}\n","/**\n * Import source registry\n *\n * Manages available import sources and provides URL probing.\n */\n\nimport { resolveAndValidateExternalUrl } from \"./ssrf.js\";\nimport type { ImportSource, ProbeResult, SourceProbeResult } from \"./types.js\";\n\n// Regex pattern for URL normalization\nconst TRAILING_SLASHES_PATTERN = /\\/+$/;\n\n/** Registered import sources */\nconst sources = new Map<string, ImportSource>();\n\n/**\n * Register an import source\n */\nexport function registerSource(source: ImportSource): void {\n\tsources.set(source.id, source);\n}\n\n/**\n * Get a source by ID\n */\nexport function getSource(id: string): ImportSource | undefined {\n\treturn sources.get(id);\n}\n\n/**\n * Get all registered sources\n */\nexport function getAllSources(): ImportSource[] {\n\treturn [...sources.values()];\n}\n\n/**\n * Get sources that can handle file uploads\n */\nexport function getFileSources(): ImportSource[] {\n\treturn getAllSources().filter((s) => s.requiresFile);\n}\n\n/**\n * Get sources that can probe URLs\n */\nexport function getUrlSources(): ImportSource[] {\n\treturn getAllSources().filter((s) => s.canProbe);\n}\n\n/**\n * Probe a URL against all registered sources\n *\n * Returns probe results sorted by confidence (definite > likely > possible)\n */\nexport async function probeUrl(url: string): Promise<ProbeResult> {\n\t// Normalize URL\n\tlet normalizedUrl = url.trim();\n\tif (!normalizedUrl.startsWith(\"http\")) {\n\t\tnormalizedUrl = `https://${normalizedUrl}`;\n\t}\n\n\t// Remove trailing slash for consistency\n\tnormalizedUrl = normalizedUrl.replace(TRAILING_SLASHES_PATTERN, \"\");\n\n\t// SSRF: reject internal/private network targets. DNS resolution\n\t// catches hostnames that resolve to private addresses.\n\tawait resolveAndValidateExternalUrl(normalizedUrl);\n\n\tconst results: SourceProbeResult[] = [];\n\tconst urlSources = getUrlSources();\n\n\t// Probe all sources in parallel\n\tconst probePromises = urlSources.map(async (source) => {\n\t\ttry {\n\t\t\tconst result = await source.probe?.(normalizedUrl);\n\t\t\tif (result) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\t// Probe failed, skip this source\n\t\t\tconsole.debug(`Probe failed for ${source.id}:`, error);\n\t\t}\n\t\treturn null;\n\t});\n\n\tconst probeResults = await Promise.allSettled(probePromises);\n\n\tfor (const result of probeResults) {\n\t\tif (result.status === \"fulfilled\" && result.value) {\n\t\t\tresults.push(result.value);\n\t\t}\n\t}\n\n\t// Sort by confidence\n\tconst confidenceOrder = { definite: 0, likely: 1, possible: 2 };\n\tresults.sort((a, b) => confidenceOrder[a.confidence] - confidenceOrder[b.confidence]);\n\n\treturn {\n\t\turl: normalizedUrl,\n\t\tisWordPress: results.length > 0,\n\t\tbestMatch: results[0] ?? null,\n\t\tallMatches: results,\n\t};\n}\n\n/**\n * Clear all registered sources (useful for testing)\n */\nexport function clearSources(): void {\n\tsources.clear();\n}\n","/**\n * WXR (WordPress eXtended RSS) import source\n *\n * Handles WordPress export file uploads (.xml).\n * This wraps the existing WXR parsing and analysis logic.\n */\n\nimport { gutenbergToPortableText } from \"@premium-cms/gutenberg-to-portable-text\";\n\nimport { parseWxrString, type WxrData, type WxrPost } from \"../../cli/wxr/parser.js\";\nimport type {\n\tImportSource,\n\tImportAnalysis,\n\tImportContext,\n\tSourceInput,\n\tFetchOptions,\n\tNormalizedItem,\n\tPostTypeAnalysis,\n\tAttachmentInfo,\n\tNavMenuAnalysis,\n\tTaxonomyAnalysis,\n\tReusableBlockAnalysis,\n} from \"../types.js\";\nimport {\n\tBASE_REQUIRED_FIELDS,\n\tFEATURED_IMAGE_FIELD,\n\tisInternalPostType,\n\tisInternalMetaKey,\n\tmapWpStatus,\n\tmapPostTypeToCollection,\n\tmapMetaKeyToField,\n\tinferMetaType,\n\tslugify,\n\tbuildAttachmentMap,\n\tgetFilenameFromUrl,\n\tguessMimeType,\n\tcheckSchemaCompatibility,\n\trelativizeContentLinks,\n} from \"../utils.js\";\n\nexport const wxrSource: ImportSource = {\n\tid: \"wxr\",\n\tname: \"WordPress Export File\",\n\tdescription: \"Upload a WordPress export file (.xml)\",\n\ticon: \"upload\",\n\trequiresFile: true,\n\tcanProbe: false,\n\n\tasync analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis> {\n\t\tif (input.type !== \"file\") {\n\t\t\tthrow new Error(\"WXR source requires a file input\");\n\t\t}\n\n\t\tconst text = await input.file.text();\n\t\tconst wxr = await parseWxrString(text);\n\n\t\t// Get existing collections for schema compatibility check\n\t\tconst existingCollections = context.getExistingCollections\n\t\t\t? await context.getExistingCollections()\n\t\t\t: new Map();\n\n\t\treturn analyzeWxrData(wxr, existingCollections);\n\t},\n\n\tasync *fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem> {\n\t\tif (input.type !== \"file\") {\n\t\t\tthrow new Error(\"WXR source requires a file input\");\n\t\t}\n\n\t\tconst text = await input.file.text();\n\t\tconst wxr = await parseWxrString(text);\n\n\t\t// Build attachment ID -> URL map for resolving featured images\n\t\tconst attachmentMap = buildAttachmentMap(wxr.attachments);\n\n\t\tlet count = 0;\n\t\tfor (const post of wxr.posts) {\n\t\t\tconst postType = post.postType || \"post\";\n\n\t\t\t// Skip if not in requested post types\n\t\t\tif (!options.postTypes.includes(postType)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Skip internal post types\n\t\t\tif (isInternalPostType(postType)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Skip drafts if not requested\n\t\t\tif (!options.includeDrafts && post.status !== \"publish\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Convert to normalized item\n\t\t\tyield wxrPostToNormalizedItem(post, attachmentMap, wxr.site.link || \"\");\n\n\t\t\tcount++;\n\t\t\tif (options.limit && count >= options.limit) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t},\n};\n\n/**\n * Analyze WXR data and return normalized ImportAnalysis\n */\nfunction analyzeWxrData(\n\twxr: WxrData,\n\texistingCollections: Map<string, { slug: string; fields: Map<string, { type: string }> }>,\n): ImportAnalysis {\n\t// Count post types and track which have featured images\n\tconst postTypeCounts = new Map<string, number>();\n\tconst postTypesWithThumbnails = new Set<string>();\n\tconst metaKeys = new Map<string, { count: number; samples: string[]; isInternal: boolean }>();\n\tconst authorPostCounts = new Map<string, number>();\n\n\tfor (const post of wxr.posts) {\n\t\tconst type = post.postType || \"post\";\n\t\tpostTypeCounts.set(type, (postTypeCounts.get(type) || 0) + 1);\n\n\t\t// Count posts per author (by login)\n\t\tif (post.creator) {\n\t\t\tauthorPostCounts.set(post.creator, (authorPostCounts.get(post.creator) || 0) + 1);\n\t\t}\n\n\t\t// Track if this post type has featured images\n\t\tif (post.meta.has(\"_thumbnail_id\")) {\n\t\t\tpostTypesWithThumbnails.add(type);\n\t\t}\n\n\t\t// Analyze meta keys\n\t\tfor (const [key, value] of post.meta) {\n\t\t\tconst existing = metaKeys.get(key);\n\t\t\tif (existing) {\n\t\t\t\texisting.count++;\n\t\t\t\tif (existing.samples.length < 3 && value) {\n\t\t\t\t\texisting.samples.push(value.slice(0, 100));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tmetaKeys.set(key, {\n\t\t\t\t\tcount: 1,\n\t\t\t\t\tsamples: value ? [value.slice(0, 100)] : [],\n\t\t\t\t\tisInternal: isInternalMetaKey(key),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t// Map meta keys to fields (for custom fields analysis)\n\tconst customFields = [...metaKeys.entries()]\n\t\t.filter(([_, info]) => !info.isInternal)\n\t\t.map(([key, info]) => ({\n\t\t\tkey,\n\t\t\tcount: info.count,\n\t\t\tsamples: info.samples,\n\t\t\tsuggestedField: mapMetaKeyToField(key),\n\t\t\tsuggestedType: inferMetaType(key, info.samples[0]),\n\t\t\tisInternal: info.isInternal,\n\t\t}))\n\t\t.toSorted((a, b) => b.count - a.count);\n\n\t// Build post type analysis with schema compatibility\n\tconst postTypes: PostTypeAnalysis[] = [...postTypeCounts.entries()]\n\t\t.filter(([type]) => !isInternalPostType(type))\n\t\t.map(([name, count]) => {\n\t\t\tconst suggestedCollection = mapPostTypeToCollection(name);\n\t\t\tconst existingCollection = existingCollections.get(suggestedCollection);\n\n\t\t\t// Build required fields - add featured_image only if posts have thumbnails\n\t\t\tconst requiredFields = [...BASE_REQUIRED_FIELDS];\n\t\t\tif (postTypesWithThumbnails.has(name)) {\n\t\t\t\trequiredFields.push(FEATURED_IMAGE_FIELD);\n\t\t\t}\n\n\t\t\tconst schemaStatus = checkSchemaCompatibility(requiredFields, existingCollection);\n\n\t\t\treturn {\n\t\t\t\tname,\n\t\t\t\tcount,\n\t\t\t\tsuggestedCollection,\n\t\t\t\trequiredFields,\n\t\t\t\tschemaStatus,\n\t\t\t};\n\t\t})\n\t\t.toSorted((a, b) => b.count - a.count);\n\n\t// Build attachment info list\n\tconst attachmentItems: AttachmentInfo[] = wxr.attachments.map((att) => {\n\t\tconst filename = att.url ? getFilenameFromUrl(att.url) : undefined;\n\t\tconst mimeType = filename ? guessMimeType(filename) : undefined;\n\t\treturn {\n\t\t\tid: att.id,\n\t\t\ttitle: att.title,\n\t\t\turl: att.url,\n\t\t\tfilename,\n\t\t\tmimeType,\n\t\t};\n\t});\n\n\t// Analyze navigation menus\n\tconst navMenus: NavMenuAnalysis[] = wxr.navMenus.map((menu) => ({\n\t\tname: menu.name,\n\t\tlabel: menu.label,\n\t\titemCount: menu.items.length,\n\t}));\n\n\t// Analyze custom taxonomies (from wp:term elements, excluding category/post_tag/nav_menu)\n\tconst taxonomyMap = new Map<string, { count: number; samples: string[] }>();\n\tfor (const term of wxr.terms) {\n\t\tif (\n\t\t\tterm.taxonomy === \"category\" ||\n\t\t\tterm.taxonomy === \"post_tag\" ||\n\t\t\tterm.taxonomy === \"nav_menu\"\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst existing = taxonomyMap.get(term.taxonomy);\n\t\tif (existing) {\n\t\t\texisting.count++;\n\t\t\tif (existing.samples.length < 3) {\n\t\t\t\texisting.samples.push(term.name);\n\t\t\t}\n\t\t} else {\n\t\t\ttaxonomyMap.set(term.taxonomy, {\n\t\t\t\tcount: 1,\n\t\t\t\tsamples: [term.name],\n\t\t\t});\n\t\t}\n\t}\n\n\tconst customTaxonomies: TaxonomyAnalysis[] = Array.from(\n\t\ttaxonomyMap.entries(),\n\t\t([slug, info]) => ({\n\t\t\tslug,\n\t\t\ttermCount: info.count,\n\t\t\tsampleTerms: info.samples,\n\t\t}),\n\t).toSorted((a, b) => b.termCount - a.termCount);\n\n\t// Analyze reusable blocks (wp_block post type)\n\tconst reusableBlocks: ReusableBlockAnalysis[] = wxr.posts\n\t\t.filter((post) => post.postType === \"wp_block\")\n\t\t.map((post) => ({\n\t\t\tid: post.id || 0,\n\t\t\ttitle: post.title || \"Untitled Block\",\n\t\t\tslug: post.postName || slugify(post.title || `block-${post.id || Date.now()}`),\n\t\t}));\n\n\treturn {\n\t\tsourceId: \"wxr\",\n\t\tsite: {\n\t\t\ttitle: wxr.site.title || \"WordPress Site\",\n\t\t\turl: wxr.site.link || \"\",\n\t\t},\n\t\tpostTypes,\n\t\tattachments: {\n\t\t\tcount: wxr.attachments.length,\n\t\t\titems: attachmentItems,\n\t\t},\n\t\tcategories: wxr.categories.length,\n\t\ttags: wxr.tags.length,\n\t\tauthors: wxr.authors.map((a) => ({\n\t\t\tid: a.id,\n\t\t\tlogin: a.login,\n\t\t\temail: a.email,\n\t\t\tdisplayName: a.displayName || a.login || \"Unknown\",\n\t\t\tpostCount: a.login ? authorPostCounts.get(a.login) || 0 : 0,\n\t\t})),\n\t\tnavMenus: navMenus.length > 0 ? navMenus : undefined,\n\t\tcustomTaxonomies: customTaxonomies.length > 0 ? customTaxonomies : undefined,\n\t\treusableBlocks: reusableBlocks.length > 0 ? reusableBlocks : undefined,\n\t\tcustomFields,\n\t};\n}\n\n/**\n * Convert a WXR post to a normalized item\n */\nfunction wxrPostToNormalizedItem(\n\tpost: WxrPost,\n\tattachmentMap: Map<string, string>,\n\tsiteUrl: string,\n): NormalizedItem {\n\tconst content = post.content ? gutenbergToPortableText(post.content) : [];\n\tif (siteUrl) relativizeContentLinks(content, siteUrl);\n\n\t// Resolve featured image: _thumbnail_id is the attachment ID, look up the URL\n\tconst thumbnailId = post.meta.get(\"_thumbnail_id\");\n\tconst featuredImage = thumbnailId ? attachmentMap.get(String(thumbnailId)) : undefined;\n\n\t// Convert custom taxonomies Map to Record.\n\t// Strip Polylang's `language` taxonomy -- the parser already promoted it\n\t// to `post.locale`, and exposing it as a content taxonomy here would\n\t// (a) try to insert a `language` taxonomy on EmDash that doesn't exist\n\t// and (b) duplicate the locale signal in two places.\n\tlet customTaxonomies: Record<string, string[]> | undefined;\n\tif (post.customTaxonomies && post.customTaxonomies.size > 0) {\n\t\tconst filtered = Object.fromEntries(\n\t\t\t[...post.customTaxonomies].filter(([taxonomy]) => taxonomy !== \"language\"),\n\t\t);\n\t\tif (Object.keys(filtered).length > 0) {\n\t\t\tcustomTaxonomies = filtered;\n\t\t}\n\t}\n\n\treturn {\n\t\tsourceId: post.id || 0,\n\t\tpostType: post.postType || \"post\",\n\t\tstatus: mapWpStatus(post.status),\n\t\tslug: post.postName || slugify(post.title || `post-${post.id || Date.now()}`),\n\t\ttitle: post.title || \"Untitled\",\n\t\tcontent,\n\t\texcerpt: post.excerpt,\n\t\tdate: parseWxrDate(post.postDateGmt, post.pubDate, post.postDate) ?? new Date(),\n\t\tmodified: parseWxrDate(post.postModifiedGmt, undefined, post.postModified),\n\t\tauthor: post.creator,\n\t\tcategories: post.categories,\n\t\ttags: post.tags,\n\t\tmeta: Object.fromEntries(post.meta),\n\t\tfeaturedImage,\n\t\t// Hierarchical content support\n\t\tparentId: post.postParent && post.postParent !== 0 ? post.postParent : undefined,\n\t\tmenuOrder: post.menuOrder,\n\t\t// Custom taxonomy assignments\n\t\tcustomTaxonomies,\n\t\t// Multilingual plugin metadata (WPML / Polylang) promoted by the parser.\n\t\t// When absent, the caller (e.g. execute route) falls back to the\n\t\t// upload-wide locale.\n\t\tlocale: post.locale,\n\t\ttranslationGroup: post.translationGroup,\n\t};\n}\n\n/**\n * WordPress uses \"0000-00-00 00:00:00\" as a sentinel for missing GMT dates\n * (e.g. unpublished drafts). This must be treated as absent.\n */\nexport const WXR_ZERO_DATE = \"0000-00-00 00:00:00\";\n\n/**\n * Parse a WXR date with the correct fallback chain:\n * 1. GMT date (always UTC, most reliable)\n * 2. pubDate (RFC 2822, includes timezone offset)\n * 3. Site-local date (MySQL datetime without timezone, imprecise but best available)\n *\n * Returns undefined when none of the inputs yield a valid date.\n * Callers that need a guaranteed Date should use `?? new Date()`.\n */\nexport function parseWxrDate(\n\tgmtDate: string | undefined,\n\tpubDate: string | undefined,\n\tlocalDate: string | undefined,\n): Date | undefined {\n\tif (gmtDate && gmtDate !== WXR_ZERO_DATE) {\n\t\t// GMT dates from WordPress are \"YYYY-MM-DD HH:MM:SS\" in UTC.\n\t\t// Append \"Z\" so the JS Date constructor treats them as UTC.\n\t\treturn new Date(gmtDate.replace(\" \", \"T\") + \"Z\");\n\t}\n\n\tif (pubDate) {\n\t\t// RFC 2822 format includes timezone offset, JS Date parses it correctly\n\t\tconst d = new Date(pubDate);\n\t\tif (!isNaN(d.getTime())) return d;\n\t}\n\n\tif (localDate) {\n\t\t// Site-local time without timezone. Normalize to ISO-like form so\n\t\t// runtimes that reject \"YYYY-MM-DD HH:MM:SS\" can still parse it as\n\t\t// local time. If parsing still fails, return undefined.\n\t\tconst d = new Date(localDate.replace(\" \", \"T\"));\n\t\tif (!isNaN(d.getTime())) return d;\n\t}\n\n\treturn undefined;\n}\n\n// Export for use in other sources\nexport { analyzeWxrData, wxrPostToNormalizedItem };\n\n// Re-export shared utilities that other sources may need\nexport {\n\tBASE_REQUIRED_FIELDS,\n\tFEATURED_IMAGE_FIELD,\n\tmapPostTypeToCollection,\n\tisInternalPostType,\n\tcheckSchemaCompatibility,\n} from \"../utils.js\";\n","/**\n * WordPress REST API probe\n *\n * Probes self-hosted WordPress sites to detect capabilities.\n * This source is probe-only - it tells users what's available\n * and suggests next steps (usually: upload WXR file).\n */\n\nimport { ssrfSafeFetch, validateExternalUrl } from \"../ssrf.js\";\nimport type {\n\tImportSource,\n\tImportAnalysis,\n\tImportContext,\n\tSourceInput,\n\tSourceProbeResult,\n\tFetchOptions,\n\tNormalizedItem,\n} from \"../types.js\";\n\nconst TRAILING_SLASHES = /\\/+$/;\nconst WP_JSON_SUFFIX = /\\/wp-json\\/?$/;\n\n/** WordPress REST API discovery response */\ninterface WpApiDiscovery {\n\tname?: string;\n\tdescription?: string;\n\turl?: string;\n\thome?: string;\n\tgmt_offset?: number;\n\ttimezone_string?: string;\n\tnamespaces?: string[];\n\tauthentication?: Record<string, unknown>;\n\troutes?: Record<string, unknown>;\n}\n\nexport const wordpressRestSource: ImportSource = {\n\tid: \"wordpress-rest\",\n\tname: \"WordPress Site\",\n\tdescription: \"Connect to a self-hosted WordPress site\",\n\ticon: \"globe\",\n\trequiresFile: false,\n\tcanProbe: true,\n\n\tasync probe(url: string): Promise<SourceProbeResult | null> {\n\t\ttry {\n\t\t\tconst siteUrl = normalizeUrl(url);\n\n\t\t\t// SSRF protection: validate URL before any outbound requests\n\t\t\tvalidateExternalUrl(siteUrl);\n\n\t\t\t// Try to fetch the WP REST API root\n\t\t\tconst apiUrl = `${siteUrl}/wp-json/`;\n\t\t\tconst response = await ssrfSafeFetch(apiUrl, {\n\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t\tsignal: AbortSignal.timeout(10000),\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\t// Try alternate location (some sites use different prefix)\n\t\t\t\tconst altResponse = await ssrfSafeFetch(`${siteUrl}/?rest_route=/`, {\n\t\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t\t\tsignal: AbortSignal.timeout(10000),\n\t\t\t\t});\n\n\t\t\t\tif (!altResponse.ok) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst data: WpApiDiscovery = await response.json();\n\n\t\t\t// Check if this looks like WordPress\n\t\t\tif (!data.namespaces?.includes(\"wp/v2\")) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Get content counts (unauthenticated - published only)\n\t\t\tconst preview = await getPublicContentCounts(siteUrl);\n\n\t\t\t// Check for authentication methods\n\t\t\tconst hasAppPasswords = !!data.authentication?.[\"application-passwords\"];\n\n\t\t\treturn {\n\t\t\t\tsourceId: \"wordpress-rest\",\n\t\t\t\tconfidence: \"definite\",\n\t\t\t\tdetected: {\n\t\t\t\t\tplatform: \"wordpress\",\n\t\t\t\t\tsiteTitle: data.name,\n\t\t\t\t\tsiteUrl: data.url || data.home || siteUrl,\n\t\t\t\t},\n\t\t\t\tcapabilities: {\n\t\t\t\t\tpublicContent: true,\n\t\t\t\t\tprivateContent: false, // Would need auth\n\t\t\t\t\tcustomPostTypes: false, // Only if show_in_rest: true\n\t\t\t\t\tallMeta: false, // Only if registered for REST\n\t\t\t\t\tmediaStream: true,\n\t\t\t\t},\n\t\t\t\tauth: hasAppPasswords\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttype: \"password\",\n\t\t\t\t\t\t\tinstructions:\n\t\t\t\t\t\t\t\t\"To import drafts and private content, create an Application Password in WordPress → Users → Your Profile → Application Passwords\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t\tpreview,\n\t\t\t\tsuggestedAction: {\n\t\t\t\t\ttype: \"upload\",\n\t\t\t\t\tinstructions:\n\t\t\t\t\t\t\"For a complete import including drafts, custom post types, and all metadata, export your content from WordPress (Tools → Export) and upload the file here.\",\n\t\t\t\t},\n\t\t\t};\n\t\t} catch {\n\t\t\t// Probe failed - not a WordPress site or not accessible\n\t\t\treturn null;\n\t\t}\n\t},\n\n\tasync analyze(_input: SourceInput, _context: ImportContext): Promise<ImportAnalysis> {\n\t\t// REST-only import not implemented - we use this for probe only\n\t\t// and suggest WXR upload for actual import\n\t\tthrow new Error(\"Direct REST API import not implemented. Please upload a WXR export file.\");\n\t},\n\n\t// eslint-disable-next-line require-yield\n\tasync *fetchContent(_input: SourceInput, _options: FetchOptions): AsyncGenerator<NormalizedItem> {\n\t\tthrow new Error(\"Direct REST API import not implemented. Please upload a WXR export file.\");\n\t},\n};\n\n/**\n * Normalize a URL for API requests\n */\nfunction normalizeUrl(url: string): string {\n\tlet normalized = url.trim();\n\n\t// Add protocol if missing\n\tif (!normalized.startsWith(\"http\")) {\n\t\tnormalized = `https://${normalized}`;\n\t}\n\n\t// Remove trailing slash\n\tnormalized = normalized.replace(TRAILING_SLASHES, \"\");\n\n\t// Remove /wp-json if included\n\tnormalized = normalized.replace(WP_JSON_SUFFIX, \"\");\n\n\treturn normalized;\n}\n\n/**\n * Get public content counts from REST API\n */\nasync function getPublicContentCounts(\n\tsiteUrl: string,\n): Promise<{ posts?: number; pages?: number; media?: number }> {\n\tconst result: { posts?: number; pages?: number; media?: number } = {};\n\n\ttry {\n\t\t// Fetch with per_page=1 to get total from headers\n\t\tconst [postsRes, pagesRes, mediaRes] = await Promise.allSettled([\n\t\t\tssrfSafeFetch(`${siteUrl}/wp-json/wp/v2/posts?per_page=1`, {\n\t\t\t\tsignal: AbortSignal.timeout(5000),\n\t\t\t}),\n\t\t\tssrfSafeFetch(`${siteUrl}/wp-json/wp/v2/pages?per_page=1`, {\n\t\t\t\tsignal: AbortSignal.timeout(5000),\n\t\t\t}),\n\t\t\tssrfSafeFetch(`${siteUrl}/wp-json/wp/v2/media?per_page=1`, {\n\t\t\t\tsignal: AbortSignal.timeout(5000),\n\t\t\t}),\n\t\t]);\n\n\t\tif (postsRes.status === \"fulfilled\" && postsRes.value.ok) {\n\t\t\tconst total = postsRes.value.headers.get(\"X-WP-Total\");\n\t\t\tif (total) result.posts = parseInt(total, 10);\n\t\t}\n\n\t\tif (pagesRes.status === \"fulfilled\" && pagesRes.value.ok) {\n\t\t\tconst total = pagesRes.value.headers.get(\"X-WP-Total\");\n\t\t\tif (total) result.pages = parseInt(total, 10);\n\t\t}\n\n\t\tif (mediaRes.status === \"fulfilled\" && mediaRes.value.ok) {\n\t\t\tconst total = mediaRes.value.headers.get(\"X-WP-Total\");\n\t\t\tif (total) result.media = parseInt(total, 10);\n\t\t}\n\t} catch {\n\t\t// Counts are optional, continue without them\n\t}\n\n\treturn result;\n}\n","/**\n * WordPress Plugin (EmDash Exporter) import source\n *\n * Connects to self-hosted WordPress sites running the EmDash Exporter plugin.\n * Provides full access to all content including drafts, custom post types, and ACF fields.\n */\n\nimport { gutenbergToPortableText } from \"@premium-cms/gutenberg-to-portable-text\";\n\nimport { encodeBase64 } from \"../../utils/base64.js\";\nimport type { PluginComment } from \"../comments.js\";\nimport type { PluginMenu } from \"../menus.js\";\nimport { ssrfSafeFetch, validateExternalUrl } from \"../ssrf.js\";\nimport type {\n\tImportSource,\n\tImportAnalysis,\n\tImportContext,\n\tSourceInput,\n\tSourceProbeResult,\n\tI18nDetection,\n\tFetchOptions,\n\tNormalizedItem,\n\tPostTypeAnalysis,\n\tAttachmentInfo,\n} from \"../types.js\";\nimport {\n\tBASE_REQUIRED_FIELDS,\n\tFEATURED_IMAGE_FIELD,\n\tmapPostTypeToCollection,\n\tmapWpStatus,\n\tnormalizeUrl,\n\tcheckSchemaCompatibility,\n\tisPluginBookkeepingMeta,\n\trelativizeContentLinks,\n\tsanitizeFieldSlug,\n} from \"../utils.js\";\n\n// =============================================================================\n// API Response Types\n// =============================================================================\n\n/** Detected i18n plugin info from the WordPress site */\ninterface PluginI18nInfo {\n\t/** Which multilingual plugin is active */\n\tplugin: \"wpml\" | \"polylang\";\n\t/** BCP 47 default locale */\n\tdefault_locale: string;\n\t/** All configured locales */\n\tlocales: string[];\n}\n\n/** Probe response from /emdash/v1/probe */\ninterface PluginProbeResponse {\n\temdash_exporter: string;\n\twordpress_version: string;\n\tsite: {\n\t\ttitle: string;\n\t\tdescription: string;\n\t\turl: string;\n\t\thome: string;\n\t\tlanguage: string;\n\t\ttimezone: string;\n\t};\n\tcapabilities: {\n\t\tapplication_passwords: boolean;\n\t\tacf: boolean;\n\t\tyoast: boolean;\n\t\trankmath: boolean;\n\t};\n\tpost_types: Array<{\n\t\tname: string;\n\t\tlabel: string;\n\t\tcount: number;\n\t}>;\n\tmedia_count: number;\n\tendpoints: Record<string, string>;\n\tauth_instructions: {\n\t\tmethod: string;\n\t\tinstructions: string;\n\t\turl?: string;\n\t};\n\t/** Detected multilingual plugin (WPML or Polylang). Absent when neither is active. */\n\ti18n?: PluginI18nInfo;\n}\n\n/** Analyze response from /emdash/v1/analyze */\ninterface PluginAnalyzeResponse {\n\tsite: {\n\t\ttitle: string;\n\t\turl: string;\n\t};\n\tpost_types: Array<{\n\t\tname: string;\n\t\tlabel: string;\n\t\tlabel_singular: string;\n\t\ttotal: number;\n\t\tby_status: Record<string, number>;\n\t\tsupports: Record<string, unknown>;\n\t\ttaxonomies: string[];\n\t\tcustom_fields: Array<{\n\t\t\tkey: string;\n\t\t\tcount: number;\n\t\t\tinferred_type: string;\n\t\t\tsample: string | null;\n\t\t}>;\n\t\thierarchical: boolean;\n\t\thas_archive: boolean;\n\t}>;\n\ttaxonomies: Array<{\n\t\tname: string;\n\t\tlabel: string;\n\t\thierarchical: boolean;\n\t\tterm_count: number;\n\t\tobject_types: string[];\n\t}>;\n\tauthors: Array<{\n\t\tid: number;\n\t\tlogin: string;\n\t\temail: string;\n\t\tdisplay_name: string;\n\t\tpost_count: number;\n\t}>;\n\tattachments: {\n\t\tcount: number;\n\t\tby_type: Record<string, number>;\n\t};\n\tacf?: Array<{\n\t\tkey: string;\n\t\ttitle: string;\n\t\tfields: Array<{\n\t\t\tkey: string;\n\t\t\tname: string;\n\t\t\tlabel: string;\n\t\t\ttype: string;\n\t\t\trequired: boolean;\n\t\t}>;\n\t}>;\n\t/** Detected multilingual plugin (WPML or Polylang). Absent when neither is active. */\n\ti18n?: PluginI18nInfo;\n}\n\n/** Content response from /emdash/v1/content */\ninterface PluginContentResponse {\n\titems: PluginPost[];\n\ttotal: number;\n\tpages: number;\n\tpage: number;\n\tper_page: number;\n}\n\n/** Single post from plugin API */\ninterface PluginPost {\n\tid: number;\n\tpost_type: string;\n\tstatus: string;\n\tslug: string;\n\ttitle: string;\n\tcontent: string;\n\texcerpt: string;\n\tdate: string;\n\tdate_gmt: string;\n\tmodified: string;\n\tmodified_gmt: string;\n\tauthor: {\n\t\tid: number;\n\t\tlogin: string;\n\t\temail: string;\n\t\tdisplay_name: string;\n\t} | null;\n\tparent: number | null;\n\tmenu_order: number;\n\ttaxonomies: Record<string, Array<{ id: number; name: string; slug: string }>>;\n\tfeatured_image?: {\n\t\tid: number;\n\t\turl: string;\n\t\tfilename: string;\n\t\tmime_type: string;\n\t\talt: string;\n\t\ttitle: string;\n\t\tcaption: string;\n\t\twidth: number | null;\n\t\theight: number | null;\n\t};\n\tmeta: Record<string, unknown>;\n\tacf?: Record<string, unknown>;\n\tyoast?: Record<string, string>;\n\trankmath?: Record<string, string>;\n\t/** BCP 47 locale from WPML/Polylang (when detected) */\n\tlocale?: string;\n\t/** Translation group ID from WPML trid or Polylang (when detected) */\n\ttranslation_group?: string;\n}\n\n/** Media response from /emdash/v1/media */\ninterface PluginMediaResponse {\n\titems: PluginMediaItem[];\n\ttotal: number;\n\tpages: number;\n\tpage: number;\n\tper_page: number;\n}\n\ninterface PluginMediaItem {\n\tid: number;\n\turl: string;\n\tfilename: string;\n\tmime_type: string;\n\ttitle: string;\n\talt: string;\n\tcaption: string;\n\tdescription: string;\n\twidth?: number;\n\theight?: number;\n\tfilesize?: number;\n}\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n/** Pattern to remove spaces from application passwords */\nconst SPACE_PATTERN = /\\s/g;\n\n/**\n * Build the REST API URL for a plugin endpoint.\n *\n * `restRoute: false` uses the pretty form (`/wp-json/emdash/v1/...`),\n * `restRoute: true` uses the `?rest_route=` form that works on sites with\n * plain permalinks (where `/wp-json/` doesn't exist).\n */\nfunction pluginApiUrl(\n\tsiteUrl: string,\n\tpath: string,\n\tparams: Record<string, string> = {},\n\trestRoute = false,\n): string {\n\tif (restRoute) {\n\t\tconst url = new URL(siteUrl + \"/\");\n\t\turl.searchParams.set(\"rest_route\", `/emdash/v1/${path}`);\n\t\tfor (const [key, value] of Object.entries(params)) {\n\t\t\turl.searchParams.set(key, value);\n\t\t}\n\t\treturn url.toString();\n\t}\n\tconst url = new URL(`${siteUrl}/wp-json/emdash/v1/${path}`);\n\tfor (const [key, value] of Object.entries(params)) {\n\t\turl.searchParams.set(key, value);\n\t}\n\treturn url.toString();\n}\n\n/**\n * Fetch a plugin API endpoint, falling back to the `?rest_route=` form when\n * the pretty `/wp-json/` route 404s or is unreachable. Sites with \"Plain\"\n * permalinks have no `/wp-json/` rewrite, so without this fallback they\n * always fail with a misleading 404.\n */\nasync function fetchPluginApi(\n\tsiteUrl: string,\n\tpath: string,\n\tparams: Record<string, string>,\n\theaders: HeadersInit,\n\ttimeoutMs: number,\n): Promise<Response> {\n\tlet pretty: Response | null = null;\n\ttry {\n\t\tpretty = await ssrfSafeFetch(pluginApiUrl(siteUrl, path, params), {\n\t\t\theaders,\n\t\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t\t});\n\t} catch {\n\t\t// Network-level failure -- try the rest_route form before giving up.\n\t}\n\t// Any response other than 404 (including 401/403/500) is authoritative:\n\t// the route exists, so don't mask the real error with a fallback attempt.\n\tif (pretty && pretty.status !== 404) {\n\t\treturn pretty;\n\t}\n\treturn ssrfSafeFetch(pluginApiUrl(siteUrl, path, params, true), {\n\t\theaders,\n\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t});\n}\n\n// =============================================================================\n// Import Source\n// =============================================================================\n\nexport const wordpressPluginSource: ImportSource = {\n\tid: \"wordpress-plugin\",\n\tname: \"WordPress (EmDash Exporter)\",\n\tdescription: \"Import from WordPress sites with the EmDash Exporter plugin installed\",\n\ticon: \"plug\",\n\trequiresFile: false,\n\tcanProbe: true,\n\n\tasync probe(url: string): Promise<SourceProbeResult | null> {\n\t\ttry {\n\t\t\tconst siteUrl = normalizeUrl(url);\n\n\t\t\t// SSRF protection: validate URL before any outbound requests\n\t\t\tvalidateExternalUrl(siteUrl);\n\n\t\t\tconst response = await fetchPluginApi(\n\t\t\t\tsiteUrl,\n\t\t\t\t\"probe\",\n\t\t\t\t{},\n\t\t\t\t{ Accept: \"application/json\" },\n\t\t\t\t10000,\n\t\t\t);\n\n\t\t\tif (!response.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst data: PluginProbeResponse = await response.json();\n\n\t\t\t// Verify it's actually our plugin\n\t\t\tif (!data.emdash_exporter) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tsourceId: \"wordpress-plugin\",\n\t\t\t\tconfidence: \"definite\",\n\t\t\t\tdetected: {\n\t\t\t\t\tplatform: \"wordpress\",\n\t\t\t\t\tversion: data.wordpress_version,\n\t\t\t\t\tsiteTitle: data.site.title,\n\t\t\t\t\tsiteUrl: data.site.url,\n\t\t\t\t},\n\t\t\t\tcapabilities: {\n\t\t\t\t\tpublicContent: true,\n\t\t\t\t\tprivateContent: true, // Full access with auth\n\t\t\t\t\tcustomPostTypes: true,\n\t\t\t\t\tallMeta: true,\n\t\t\t\t\tmediaStream: true,\n\t\t\t\t},\n\t\t\t\tauth: data.capabilities.application_passwords\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttype: \"password\",\n\t\t\t\t\t\t\tinstructions: data.auth_instructions.instructions,\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t\tpreview: {\n\t\t\t\t\tposts: data.post_types.find((p) => p.name === \"post\")?.count,\n\t\t\t\t\tpages: data.post_types.find((p) => p.name === \"page\")?.count,\n\t\t\t\t\tmedia: data.media_count,\n\t\t\t\t},\n\t\t\t\tsuggestedAction: {\n\t\t\t\t\ttype: \"proceed\",\n\t\t\t\t},\n\t\t\t\ti18n: pluginI18nToDetection(data.i18n),\n\t\t\t};\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t},\n\n\tasync analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis> {\n\t\tconst { siteUrl, headers } = getRequestConfig(input);\n\n\t\tconst response = await fetchPluginApi(siteUrl, \"analyze\", {}, headers, 30000);\n\n\t\tif (!response.ok) {\n\t\t\tconst body: unknown = await response.json().catch(() => undefined);\n\t\t\tconst message =\n\t\t\t\ttypeof body === \"object\" &&\n\t\t\t\tbody !== null &&\n\t\t\t\t\"message\" in body &&\n\t\t\t\ttypeof body.message === \"string\"\n\t\t\t\t\t? body.message\n\t\t\t\t\t: \"\";\n\t\t\tthrow new Error(message || `Failed to analyze site: ${response.statusText}`);\n\t\t}\n\n\t\tconst data: PluginAnalyzeResponse = await response.json();\n\n\t\t// Get existing collections for schema check\n\t\tconst existingCollections = context.getExistingCollections\n\t\t\t? await context.getExistingCollections()\n\t\t\t: new Map();\n\n\t\t// Build post type analysis\n\t\tconst postTypes: PostTypeAnalysis[] = data.post_types\n\t\t\t.filter((pt) => pt.total > 0)\n\t\t\t.map((pt) => {\n\t\t\t\tconst suggestedCollection = mapPostTypeToCollection(pt.name);\n\t\t\t\tconst existingCollection = existingCollections.get(suggestedCollection);\n\n\t\t\t\t// Include featured_image if post type supports thumbnails\n\t\t\t\tconst supportsThumbnail = pt.supports && \"thumbnail\" in pt.supports;\n\t\t\t\tconst requiredFields = supportsThumbnail\n\t\t\t\t\t? [...BASE_REQUIRED_FIELDS, FEATURED_IMAGE_FIELD]\n\t\t\t\t\t: [...BASE_REQUIRED_FIELDS];\n\n\t\t\t\t// Surface the post type's custom fields (ACF and plain meta) so\n\t\t\t\t// the prepare step creates them — without this, execute() has no\n\t\t\t\t// matching schema fields and silently drops the values.\n\t\t\t\tconst knownSlugs = new Set(requiredFields.map((f) => f.slug));\n\t\t\t\tfor (const customField of pt.custom_fields ?? []) {\n\t\t\t\t\tif (isPluginBookkeepingMeta(customField.key)) continue;\n\t\t\t\t\tconst slug = sanitizeFieldSlug(customField.key);\n\t\t\t\t\tif (knownSlugs.has(slug)) continue;\n\t\t\t\t\tknownSlugs.add(slug);\n\t\t\t\t\trequiredFields.push({\n\t\t\t\t\t\tslug,\n\t\t\t\t\t\tlabel: fieldLabelFromKey(customField.key),\n\t\t\t\t\t\ttype: mapInferredFieldType(customField.inferred_type),\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\tname: pt.name,\n\t\t\t\t\tcount: pt.total,\n\t\t\t\t\tsuggestedCollection,\n\t\t\t\t\trequiredFields,\n\t\t\t\t\tschemaStatus: checkSchemaCompatibility(requiredFields, existingCollection),\n\t\t\t\t};\n\t\t\t});\n\n\t\t// Fetch the full media list, paginated. Stopping after the first page\n\t\t// silently capped imports at 500 attachments (wp-emdash #1).\n\t\tconst attachments: AttachmentInfo[] = [];\n\t\tif (data.attachments.count > 0) {\n\t\t\ttry {\n\t\t\t\tlet page = 1;\n\t\t\t\tlet totalPages = 1;\n\t\t\t\twhile (page <= totalPages) {\n\t\t\t\t\tconst mediaResponse = await fetchPluginApi(\n\t\t\t\t\t\tsiteUrl,\n\t\t\t\t\t\t\"media\",\n\t\t\t\t\t\t{ per_page: \"500\", page: String(page) },\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\t30000,\n\t\t\t\t\t);\n\t\t\t\t\tif (!mediaResponse.ok) break;\n\t\t\t\t\tconst mediaData: PluginMediaResponse = await mediaResponse.json();\n\t\t\t\t\ttotalPages = mediaData.pages;\n\t\t\t\t\tfor (const item of mediaData.items) {\n\t\t\t\t\t\tattachments.push({\n\t\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\t\turl: item.url,\n\t\t\t\t\t\t\tfilename: item.filename,\n\t\t\t\t\t\t\tmimeType: item.mime_type,\n\t\t\t\t\t\t\ttitle: item.title,\n\t\t\t\t\t\t\talt: item.alt,\n\t\t\t\t\t\t\tcaption: item.caption,\n\t\t\t\t\t\t\twidth: item.width,\n\t\t\t\t\t\t\theight: item.height,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tpage++;\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tconsole.warn(\"Failed to fetch media list:\", e);\n\t\t\t}\n\t\t}\n\n\t\t// Count categories and tags\n\t\tconst categoryTaxonomy = data.taxonomies.find((t) => t.name === \"category\");\n\t\tconst tagTaxonomy = data.taxonomies.find((t) => t.name === \"post_tag\");\n\n\t\treturn {\n\t\t\tsourceId: \"wordpress-plugin\",\n\t\t\tsite: {\n\t\t\t\ttitle: data.site.title,\n\t\t\t\turl: data.site.url,\n\t\t\t},\n\t\t\tpostTypes,\n\t\t\tattachments: {\n\t\t\t\tcount: data.attachments.count,\n\t\t\t\titems: attachments,\n\t\t\t},\n\t\t\tcategories: categoryTaxonomy?.term_count ?? 0,\n\t\t\ttags: tagTaxonomy?.term_count ?? 0,\n\t\t\tauthors: data.authors.map((a) => ({\n\t\t\t\tid: a.id,\n\t\t\t\tlogin: a.login,\n\t\t\t\temail: a.email,\n\t\t\t\tdisplayName: a.display_name,\n\t\t\t\tpostCount: a.post_count,\n\t\t\t})),\n\t\t\ti18n: pluginI18nToDetection(data.i18n),\n\t\t};\n\t},\n\n\tasync *fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem> {\n\t\tconst { siteUrl, headers } = getRequestConfig(input);\n\n\t\tfor (const postType of options.postTypes) {\n\t\t\tlet page = 1;\n\t\t\tlet totalPages = 1;\n\t\t\tlet yielded = 0;\n\n\t\t\twhile (page <= totalPages) {\n\t\t\t\tconst status = options.includeDrafts ? \"any\" : \"publish\";\n\t\t\t\tconst response = await fetchPluginApi(\n\t\t\t\t\tsiteUrl,\n\t\t\t\t\t\"content\",\n\t\t\t\t\t{ post_type: postType, status, per_page: \"100\", page: String(page) },\n\t\t\t\t\theaders,\n\t\t\t\t\t60000,\n\t\t\t\t);\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Failed to fetch ${postType}: ${response.statusText}`);\n\t\t\t\t}\n\n\t\t\t\tconst data: PluginContentResponse = await response.json();\n\t\t\t\ttotalPages = data.pages;\n\n\t\t\t\tfor (const post of data.items) {\n\t\t\t\t\tyield pluginPostToNormalizedItem(post, siteUrl);\n\t\t\t\t\tyielded++;\n\n\t\t\t\t\tif (options.limit && yielded >= options.limit) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tpage++;\n\t\t\t}\n\t\t}\n\t},\n\n\tasync fetchMedia(url: string, _input: SourceInput): Promise<Blob> {\n\t\t// SSRF protection: validate media URL before fetching\n\t\tvalidateExternalUrl(url);\n\n\t\t// Media URLs are publicly accessible on WP (ssrfSafeFetch validates redirects)\n\t\tconst response = await ssrfSafeFetch(url);\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to fetch media: ${response.statusText}`);\n\t\t}\n\t\treturn response.blob();\n\t},\n};\n\n/**\n * Fetch a single page of content for one post type. This is the unit of\n * work for the chunked import: one Worker invocation imports one page,\n * keeping each request far below Cloudflare's CPU and subrequest limits\n * (see issue #475).\n */\nexport async function fetchPluginContentPage(options: {\n\tsiteUrl: string;\n\ttoken: string;\n\tpostType: string;\n\tpage: number;\n\tperPage: number;\n\tincludeDrafts: boolean;\n}): Promise<{ items: NormalizedItem[]; totalPages: number }> {\n\tconst { siteUrl, headers } = getRequestConfig({\n\t\ttype: \"url\",\n\t\turl: options.siteUrl,\n\t\ttoken: options.token,\n\t});\n\n\tconst response = await fetchPluginApi(\n\t\tsiteUrl,\n\t\t\"content\",\n\t\t{\n\t\t\tpost_type: options.postType,\n\t\t\tstatus: options.includeDrafts ? \"any\" : \"publish\",\n\t\t\tper_page: String(options.perPage),\n\t\t\tpage: String(options.page),\n\t\t},\n\t\theaders,\n\t\t60000,\n\t);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch ${options.postType}: ${response.statusText}`);\n\t}\n\n\tconst data: PluginContentResponse = await response.json();\n\treturn {\n\t\titems: data.items.map((post) => pluginPostToNormalizedItem(post, siteUrl)),\n\t\ttotalPages: data.pages,\n\t};\n}\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/** Plugin `inferred_type` values that are valid EmDash field types as-is */\nconst VALID_INFERRED_TYPES = new Set([\n\t\"string\",\n\t\"text\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"json\",\n\t\"reference\",\n]);\n\n/**\n * Map the plugin's inferred custom-field type to an EmDash field type.\n * Unknown values fall back to string (always safe for TEXT storage).\n */\nfunction mapInferredFieldType(inferredType: string): string {\n\treturn VALID_INFERRED_TYPES.has(inferredType) ? inferredType : \"string\";\n}\n\nconst FIELD_KEY_SEPARATORS = /[_-]+/;\n\n/** Derive a human label from a meta key: \"event_start-date\" -> \"Event Start Date\" */\nfunction fieldLabelFromKey(key: string): string {\n\treturn key\n\t\t.split(FIELD_KEY_SEPARATORS)\n\t\t.filter(Boolean)\n\t\t.map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n\t\t.join(\" \");\n}\n\n/**\n * Convert plugin i18n info to the shared I18nDetection type.\n * Returns undefined when no multilingual plugin is detected.\n */\nfunction pluginI18nToDetection(i18n: PluginI18nInfo | undefined): I18nDetection | undefined {\n\tif (!i18n) return undefined;\n\treturn {\n\t\tplugin: i18n.plugin,\n\t\tdefaultLocale: i18n.default_locale,\n\t\tlocales: i18n.locales,\n\t};\n}\n\n/**\n * Get request configuration from input\n */\nfunction getRequestConfig(input: SourceInput): {\n\tsiteUrl: string;\n\theaders: HeadersInit;\n} {\n\tif (input.type === \"url\") {\n\t\tconst siteUrl = normalizeUrl(input.url);\n\n\t\t// SSRF protection: validate URL before any outbound requests\n\t\tvalidateExternalUrl(siteUrl);\n\t\tconst headers: HeadersInit = {\n\t\t\tAccept: \"application/json\",\n\t\t};\n\n\t\tif (input.token) {\n\t\t\t// Token format: \"username:password\" base64 encoded\n\t\t\theaders[\"Authorization\"] = `Basic ${input.token}`;\n\t\t}\n\n\t\treturn { siteUrl, headers };\n\t}\n\n\tif (input.type === \"oauth\") {\n\t\tconst oauthSiteUrl = normalizeUrl(input.url);\n\n\t\t// SSRF protection: validate URL before any outbound requests\n\t\tvalidateExternalUrl(oauthSiteUrl);\n\n\t\treturn {\n\t\t\tsiteUrl: oauthSiteUrl,\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${input.accessToken}`,\n\t\t\t},\n\t\t};\n\t}\n\n\tthrow new Error(\"WordPress plugin source requires URL or OAuth input\");\n}\n\n/**\n * Convert plugin post to normalized item\n */\nfunction pluginPostToNormalizedItem(post: PluginPost, siteUrl: string): NormalizedItem {\n\tconst content = post.content ? gutenbergToPortableText(post.content) : [];\n\trelativizeContentLinks(content, siteUrl);\n\n\t// Extract categories and tags from taxonomies\n\tconst categories =\n\t\tpost.taxonomies?.category?.map((c) => c.slug) ??\n\t\tpost.taxonomies?.categories?.map((c) => c.slug) ??\n\t\t[];\n\tconst tags =\n\t\tpost.taxonomies?.post_tag?.map((t) => t.slug) ??\n\t\tpost.taxonomies?.tags?.map((t) => t.slug) ??\n\t\t[];\n\n\t// Everything else is a custom taxonomy assignment (genre, product_cat, ...)\n\tconst customTaxonomies: Record<string, string[]> = {};\n\tfor (const [name, terms] of Object.entries(post.taxonomies ?? {})) {\n\t\tif ([\"category\", \"categories\", \"post_tag\", \"tags\"].includes(name)) continue;\n\t\tif (Array.isArray(terms) && terms.length > 0) {\n\t\t\tcustomTaxonomies[name] = terms.map((t) => t.slug);\n\t\t}\n\t}\n\n\t// Build meta from various sources\n\tconst meta: Record<string, unknown> = { ...post.meta };\n\n\t// Include ACF fields in meta\n\tif (post.acf) {\n\t\tmeta._acf = post.acf;\n\t}\n\n\t// Include SEO data in meta\n\tif (post.yoast) {\n\t\tmeta._yoast = post.yoast;\n\t}\n\tif (post.rankmath) {\n\t\tmeta._rankmath = post.rankmath;\n\t}\n\n\treturn {\n\t\tsourceId: post.id,\n\t\tpostType: post.post_type,\n\t\tstatus: mapWpStatus(post.status),\n\t\tslug: post.slug,\n\t\ttitle: post.title,\n\t\tcontent,\n\t\texcerpt: post.excerpt || undefined,\n\t\tdate: new Date(post.date_gmt || post.date),\n\t\tmodified: post.modified_gmt ? new Date(post.modified_gmt) : new Date(post.modified),\n\t\tauthor: post.author?.login,\n\t\tcategories,\n\t\ttags,\n\t\tcustomTaxonomies: Object.keys(customTaxonomies).length > 0 ? customTaxonomies : undefined,\n\t\tmeta,\n\t\tfeaturedImage: post.featured_image?.url,\n\t\tlocale: post.locale,\n\t\ttranslationGroup: post.translation_group,\n\t};\n}\n\n// =============================================================================\n// Utility Functions for External Use\n// =============================================================================\n\n/**\n * Create a Basic Auth token from username and password\n */\nexport function createBasicAuthToken(username: string, password: string): string {\n\t// Remove spaces from application password (WP formats them with spaces)\n\tconst cleanPassword = password.replace(SPACE_PATTERN, \"\");\n\treturn encodeBase64(`${username}:${cleanPassword}`);\n}\n\n/**\n * Fetch media list from plugin API\n */\nexport async function fetchPluginMedia(\n\tsiteUrl: string,\n\tauthToken: string,\n\tpage = 1,\n\tperPage = 100,\n): Promise<PluginMediaResponse> {\n\tconst normalizedSiteUrl = normalizeUrl(siteUrl);\n\n\t// SSRF protection: validate URL before any outbound requests\n\tvalidateExternalUrl(normalizedSiteUrl);\n\n\tconst response = await fetchPluginApi(\n\t\tnormalizedSiteUrl,\n\t\t\"media\",\n\t\t{ per_page: String(perPage), page: String(page) },\n\t\t{ Accept: \"application/json\", Authorization: `Basic ${authToken}` },\n\t\t30000,\n\t);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch media: ${response.statusText}`);\n\t}\n\n\treturn response.json();\n}\n\n/**\n * Fetch taxonomies from plugin API\n */\nexport async function fetchPluginTaxonomies(\n\tsiteUrl: string,\n\tauthToken: string,\n): Promise<\n\tArray<{\n\t\tname: string;\n\t\tlabel: string;\n\t\t/** Singular label (added in emdash-exporter 1.2.0) */\n\t\tlabel_singular?: string;\n\t\thierarchical: boolean;\n\t\t/** WP post types this taxonomy is registered for (added in emdash-exporter 1.2.0) */\n\t\tpost_types?: string[];\n\t\tterms: Array<{\n\t\t\tid: number;\n\t\t\tname: string;\n\t\t\tslug: string;\n\t\t\tdescription: string;\n\t\t\tparent: number | null;\n\t\t\tcount: number;\n\t\t}>;\n\t}>\n> {\n\tconst normalizedSiteUrl = normalizeUrl(siteUrl);\n\n\t// SSRF protection: validate URL before any outbound requests\n\tvalidateExternalUrl(normalizedSiteUrl);\n\n\tconst response = await fetchPluginApi(\n\t\tnormalizedSiteUrl,\n\t\t\"taxonomies\",\n\t\t{},\n\t\t{ Accept: \"application/json\", Authorization: `Basic ${authToken}` },\n\t\t30000,\n\t);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch taxonomies: ${response.statusText}`);\n\t}\n\n\treturn response.json();\n}\n\n/**\n * Fetch navigation menus from plugin API (added in emdash-exporter 1.1.0).\n * Returns an empty array when the endpoint doesn't exist (older plugin).\n */\nexport async function fetchPluginMenus(siteUrl: string, authToken: string): Promise<PluginMenu[]> {\n\tconst normalizedSiteUrl = normalizeUrl(siteUrl);\n\n\t// SSRF protection: validate URL before any outbound requests\n\tvalidateExternalUrl(normalizedSiteUrl);\n\n\tconst response = await fetchPluginApi(\n\t\tnormalizedSiteUrl,\n\t\t\"menus\",\n\t\t{},\n\t\t{ Accept: \"application/json\", Authorization: `Basic ${authToken}` },\n\t\t30000,\n\t);\n\n\tif (response.status === 404) {\n\t\treturn [];\n\t}\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch menus: ${response.statusText}`);\n\t}\n\n\treturn response.json();\n}\n\n/**\n * Fetch site options from plugin API (title, tagline, logo, favicon, ...)\n */\nexport async function fetchPluginOptions(\n\tsiteUrl: string,\n\tauthToken: string,\n): Promise<Record<string, unknown>> {\n\tconst normalizedSiteUrl = normalizeUrl(siteUrl);\n\n\t// SSRF protection: validate URL before any outbound requests\n\tvalidateExternalUrl(normalizedSiteUrl);\n\n\tconst response = await fetchPluginApi(\n\t\tnormalizedSiteUrl,\n\t\t\"options\",\n\t\t{},\n\t\t{ Accept: \"application/json\", Authorization: `Basic ${authToken}` },\n\t\t30000,\n\t);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch options: ${response.statusText}`);\n\t}\n\n\treturn response.json();\n}\n\n/** Comments response from /emdash/v1/comments */\ninterface PluginCommentsResponse {\n\titems: PluginComment[];\n\ttotal: number;\n\tpages: number;\n\tpage: number;\n\tper_page: number;\n}\n\n/**\n * Fetch a single page of comments from the plugin API (added in\n * emdash-exporter 1.2.0). The exporter orders by comment ID ascending, so\n * parents always appear before their children across pages. Returns\n * `totalPages: 0` when the endpoint doesn't exist (older plugin).\n */\nexport async function fetchPluginCommentsPage(\n\tsiteUrl: string,\n\tauthToken: string,\n\tpage: number,\n): Promise<{ items: PluginComment[]; totalPages: number }> {\n\tconst normalizedSiteUrl = normalizeUrl(siteUrl);\n\n\t// SSRF protection: validate URL before any outbound requests\n\tvalidateExternalUrl(normalizedSiteUrl);\n\n\tconst response = await fetchPluginApi(\n\t\tnormalizedSiteUrl,\n\t\t\"comments\",\n\t\t{ per_page: \"500\", page: String(page) },\n\t\t{ Accept: \"application/json\", Authorization: `Basic ${authToken}` },\n\t\t30000,\n\t);\n\n\tif (response.status === 404) {\n\t\treturn { items: [], totalPages: 0 };\n\t}\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch comments: ${response.statusText}`);\n\t}\n\n\tconst data: PluginCommentsResponse = await response.json();\n\treturn { items: data.items, totalPages: data.pages };\n}\n\n/**\n * Fetch all comments from plugin API, paginating through every page.\n * Returns an empty array when the endpoint doesn't exist (older plugin).\n */\nexport async function fetchPluginComments(\n\tsiteUrl: string,\n\tauthToken: string,\n): Promise<PluginComment[]> {\n\tconst comments: PluginComment[] = [];\n\tlet page = 1;\n\tlet totalPages = 1;\n\n\twhile (page <= totalPages) {\n\t\tconst result = await fetchPluginCommentsPage(siteUrl, authToken, page);\n\t\ttotalPages = result.totalPages;\n\t\tcomments.push(...result.items);\n\t\tpage++;\n\t}\n\n\treturn comments;\n}\n","/**\n * Import system\n *\n * Provides a pluggable system for importing content from various sources.\n */\n\n// Core types\nexport type {\n\tImportSource,\n\tImportAnalysis,\n\tImportContext,\n\tSourceInput,\n\tFileInput,\n\tUrlInput,\n\tOAuthInput,\n\tSourceProbeResult,\n\tProbeResult,\n\tSourceAuth,\n\tSourceCapabilities,\n\tSuggestedAction,\n\tPostTypeAnalysis,\n\tImportFieldDef,\n\tFieldCompatibility,\n\tCollectionSchemaStatus,\n\tAttachmentInfo,\n\tNormalizedItem,\n\tImportConfig,\n\tImportResult,\n\tFetchOptions,\n\tPostTypeMapping,\n\tNavMenuAnalysis,\n\tTaxonomyAnalysis,\n} from \"./types.js\";\n\n// Menu import\nexport {\n\timportMenusFromWxr,\n\timportMenusFromPlugin,\n\ttype MenuImportResult,\n\ttype PluginMenu,\n\ttype PluginMenuItem,\n} from \"./menus.js\";\n\n// Sections import\nexport { importReusableBlocksAsSections, type SectionsImportResult } from \"./sections.js\";\n\n// Comments import\nexport {\n\timportCommentsFromPlugin,\n\ttype PluginComment,\n\ttype CommentsImportResult,\n} from \"./comments.js\";\n\n// Site settings import\nexport {\n\timportSiteSettings,\n\tparseSiteSettingsFromPlugin,\n\ttype SiteSettingsAnalysis,\n\ttype SettingsImportResult,\n\ttype SettingsMediaIds,\n} from \"./settings.js\";\n\n// Registry\nexport {\n\tregisterSource,\n\tgetSource,\n\tgetAllSources,\n\tgetFileSources,\n\tgetUrlSources,\n\tprobeUrl,\n\tclearSources,\n} from \"./registry.js\";\n\n// SSRF protection\nexport { validateExternalUrl, ssrfSafeFetch, SsrfError } from \"./ssrf.js\";\n\n// Sources\nexport { wxrSource, parseWxrDate } from \"./sources/wxr.js\";\nexport { wordpressRestSource } from \"./sources/wordpress-rest.js\";\nexport {\n\twordpressPluginSource,\n\tcreateBasicAuthToken,\n\tfetchPluginMedia,\n\tfetchPluginTaxonomies,\n} from \"./sources/wordpress-plugin.js\";\n\n// Auto-register built-in sources\nimport { registerSource } from \"./registry.js\";\nimport { wordpressPluginSource } from \"./sources/wordpress-plugin.js\";\nimport { wordpressRestSource } from \"./sources/wordpress-rest.js\";\nimport { wxrSource } from \"./sources/wxr.js\";\n\n// Register in priority order (most specific first)\n// Plugin source first - if they have our plugin, use it\nregisterSource(wordpressPluginSource);\nregisterSource(wordpressRestSource);\nregisterSource(wxrSource);\n"],"mappings":"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"}