@braze/sanity-plugin-ai-translation 0.2.64 → 0.2.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import defaults from "lodash.defaults";
6
6
  import { Card, Stack, Flex, Text, Box, Heading, Button, Spinner, Inline, Checkbox, Grid, Radio, TextInput, Badge, Select, Label, Code, Tooltip } from "@sanity/ui";
7
7
  import { toPlainText } from "@portabletext/react";
8
8
  import { uuid } from "@sanity/uuid";
9
+ import { XMLParser } from "fast-xml-parser";
9
10
  import cloneDeep from "lodash.clonedeep";
10
11
  import get from "lodash.get";
11
12
  import set from "lodash.set";
@@ -1539,7 +1540,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1539
1540
  cleanedLength: cleanedText.length,
1540
1541
  preview: cleanedText.substring(0, 500)
1541
1542
  });
1542
- const parsed = parseReviewFormat(cleanedText, originalDoc);
1543
+ const parsed = parseResponseToNodes(cleanedText, originalDoc);
1543
1544
  devLog.debug("Parsed response structure", {
1544
1545
  rootNodeCount: parsed.length,
1545
1546
  rootNodeNames: parsed.map((n) => n.type === "field" && n.name ? n.name : "unknown")
@@ -1561,7 +1562,77 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1561
1562
  codeBlocksRemoved,
1562
1563
  preview: cleaned.trim().substring(0, 200)
1563
1564
  }), cleaned.trim();
1564
- }, parseReviewFormat = (text, originalDoc) => {
1565
+ }, TAG_NAMES = ["field", "block", "span"], looksLikeXmlResponse = (text) => TAG_NAMES.some((tag) => text.includes(`<${tag}`)), parseResponseToNodes = (text, originalDoc) => {
1566
+ if (looksLikeXmlResponse(text))
1567
+ try {
1568
+ const wrapped = `<root>${text}</root>`, parsed = new XMLParser({
1569
+ ignoreAttributes: !1,
1570
+ attributeNamePrefix: "@_",
1571
+ textNodeName: "#text",
1572
+ parseTagValue: !1,
1573
+ trimValues: !1,
1574
+ // preserve exact whitespace in text (e.g. trailing space in spans)
1575
+ isArray: (tagName) => TAG_NAMES.includes(tagName)
1576
+ }).parse(wrapped);
1577
+ if (parsed && typeof parsed == "object" && parsed.root) {
1578
+ const nodes = convertXmlObjectToParsedNodes(parsed.root);
1579
+ if (nodes.length > 0)
1580
+ return devLog.debug("Parsed response with XML parser", { rootNodeCount: nodes.length }), nodes;
1581
+ }
1582
+ } catch (err) {
1583
+ devLog.warn("XML parse failed, falling back to regex parser", {
1584
+ error: err instanceof Error ? err.message : String(err)
1585
+ });
1586
+ }
1587
+ return parseReviewFormat(text, originalDoc);
1588
+ };
1589
+ function normalizeToArray(value) {
1590
+ return Array.isArray(value) ? value : value != null && typeof value == "object" ? [value] : [];
1591
+ }
1592
+ function convertXmlObjectToParsedNodes(root) {
1593
+ const result = [];
1594
+ for (const tagName of TAG_NAMES) {
1595
+ const raw = root[tagName];
1596
+ if (raw !== void 0) {
1597
+ for (const item of normalizeToArray(raw))
1598
+ if (item != null && typeof item == "object") {
1599
+ const node = convertXmlElementToParsedNode(item, tagName);
1600
+ node && result.push(node);
1601
+ }
1602
+ }
1603
+ }
1604
+ return result;
1605
+ }
1606
+ function convertXmlElementToParsedNode(obj, tagName) {
1607
+ const attributes = {};
1608
+ for (const [key, value] of Object.entries(obj))
1609
+ if (key.startsWith("@_")) {
1610
+ const attrName = key.slice(2);
1611
+ attributes[attrName] = value != null ? String(value) : "";
1612
+ }
1613
+ let text;
1614
+ const textVal = obj["#text"];
1615
+ textVal != null && (text = typeof textVal == "string" ? textVal : String(textVal));
1616
+ const children = [];
1617
+ for (const childTag of TAG_NAMES) {
1618
+ const raw = obj[childTag];
1619
+ if (raw !== void 0) {
1620
+ for (const item of normalizeToArray(raw))
1621
+ if (item != null && typeof item == "object") {
1622
+ const child = convertXmlElementToParsedNode(item, childTag);
1623
+ child && children.push(child);
1624
+ }
1625
+ }
1626
+ }
1627
+ return {
1628
+ type: tagName,
1629
+ name: attributes.name,
1630
+ text: text !== void 0 && text !== "" ? unescapeXml(text) : void 0,
1631
+ attributes,
1632
+ children
1633
+ };
1634
+ }
1635
+ const parseReviewFormat = (text, originalDoc) => {
1565
1636
  const root = [], stack = [], lines = text.split(`
1566
1637
  `);
1567
1638
  devLog.debug("Parsing response text", {
@@ -1897,10 +1968,22 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1897
1968
  });
1898
1969
  return;
1899
1970
  }
1900
- node.text !== void 0 && !node.attributes.readonly && (get(doc, path) !== void 0 ? set(doc, path, node.text) : devLog.debug("Skipping field not present in original document", {
1901
- path: path.join("."),
1902
- attemptedValue: node.text
1903
- }));
1971
+ if (node.text !== void 0 && !node.attributes.readonly) {
1972
+ const existingValue = get(doc, path);
1973
+ if (existingValue === void 0)
1974
+ devLog.debug("Skipping field not present in original document", {
1975
+ path: path.join("."),
1976
+ attemptedValue: node.text
1977
+ });
1978
+ else if (typeof existingValue == "string") {
1979
+ const value = path.length === 1 ? node.text.trim() : node.text;
1980
+ set(doc, path, value);
1981
+ } else
1982
+ devLog.warn("Skipping text apply at path where value is not a string (prevents structure corruption)", {
1983
+ path: path.join("."),
1984
+ actualType: typeof existingValue
1985
+ });
1986
+ }
1904
1987
  }, applyBlockChanges = (block, blockNode) => {
1905
1988
  if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
1906
1989
  devLog.warn("Block key mismatch, skipping", {