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

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,7 +6,6 @@ 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";
10
9
  import cloneDeep from "lodash.clonedeep";
11
10
  import get from "lodash.get";
12
11
  import set from "lodash.set";
@@ -1540,7 +1539,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1540
1539
  cleanedLength: cleanedText.length,
1541
1540
  preview: cleanedText.substring(0, 500)
1542
1541
  });
1543
- const parsed = parseResponseToNodes(cleanedText, originalDoc);
1542
+ const parsed = parseReviewFormat(cleanedText, originalDoc);
1544
1543
  devLog.debug("Parsed response structure", {
1545
1544
  rootNodeCount: parsed.length,
1546
1545
  rootNodeNames: parsed.map((n) => n.type === "field" && n.name ? n.name : "unknown")
@@ -1562,77 +1561,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1562
1561
  codeBlocksRemoved,
1563
1562
  preview: cleaned.trim().substring(0, 200)
1564
1563
  }), cleaned.trim();
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) => {
1564
+ }, parseReviewFormat = (text, originalDoc) => {
1636
1565
  const root = [], stack = [], lines = text.split(`
1637
1566
  `);
1638
1567
  devLog.debug("Parsing response text", {
@@ -1720,25 +1649,34 @@ const parseReviewFormat = (text, originalDoc) => {
1720
1649
  attributes,
1721
1650
  children: []
1722
1651
  }, closingTag = `</${tagName}>`;
1723
- if (content.endsWith(closingTag.replace(/[<>]/g, ""))) {
1724
- const textContent = content.substring(0, content.lastIndexOf("<"));
1725
- if (node.text = unescapeXml(textContent), stack.length > 0) {
1652
+ if (content.includes(closingTag)) {
1653
+ const closeIdx = content.indexOf(closingTag);
1654
+ node.text = unescapeXml(content.substring(0, closeIdx));
1655
+ const remainder = content.substring(closeIdx + closingTag.length).trim();
1656
+ if (stack.length > 0) {
1726
1657
  const parent = stack[stack.length - 1];
1727
1658
  parent && parent.children.push(node);
1728
1659
  } else
1729
1660
  root.push(node);
1730
- } else if (content.includes(closingTag))
1731
- if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
1661
+ remainder && (lines[i] = remainder, i--);
1662
+ } else if (content.endsWith(closingTag)) {
1663
+ const textContent = content.substring(0, content.lastIndexOf("<"));
1664
+ if (node.text = unescapeXml(textContent), stack.length > 0) {
1732
1665
  const parent = stack[stack.length - 1];
1733
1666
  parent && parent.children.push(node);
1734
1667
  } else
1735
1668
  root.push(node);
1736
- else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
1669
+ } else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
1737
1670
  const contentLines = [];
1738
1671
  for (i++; i < lines.length; ) {
1739
1672
  const currentLine = lines[i];
1740
- if (currentLine.trim().match(/^<\/field>$/))
1673
+ if (currentLine.includes("</field>")) {
1674
+ const idx = currentLine.indexOf("</field>"), before = currentLine.substring(0, idx).trimEnd();
1675
+ before.trim() && contentLines.push(before);
1676
+ const after = currentLine.substring(idx + 8).trim();
1677
+ after && (lines[i] = after, i--);
1741
1678
  break;
1679
+ }
1742
1680
  contentLines.push(currentLine), i++;
1743
1681
  }
1744
1682
  if (node.text = contentLines.join(`
@@ -1747,19 +1685,15 @@ const parseReviewFormat = (text, originalDoc) => {
1747
1685
  parent && parent.children.push(node);
1748
1686
  } else
1749
1687
  root.push(node);
1750
- } else if (attributes.type === "string" && content && content.includes(closingTag))
1751
- if (node.text = unescapeXml(content.replace(closingTag, "")), stack.length > 0) {
1752
- const parent = stack[stack.length - 1];
1753
- parent && parent.children.push(node);
1754
- } else
1755
- root.push(node);
1756
- else if (attributes.type === "string" && content && !content.includes(closingTag)) {
1688
+ } else if (attributes.type === "string" && content && !content.includes(closingTag)) {
1757
1689
  const contentLines = [content];
1758
1690
  for (i++; i < lines.length; ) {
1759
1691
  const currentLine = lines[i];
1760
1692
  if (currentLine.includes(closingTag)) {
1761
- const closingIndex = currentLine.indexOf(closingTag), remainingContent = currentLine.substring(0, closingIndex);
1693
+ const closingIndex = currentLine.indexOf(closingTag), remainingContent = currentLine.substring(0, closingIndex).trimEnd();
1762
1694
  remainingContent && contentLines.push(remainingContent);
1695
+ const after = currentLine.substring(closingIndex + closingTag.length).trim();
1696
+ after && (lines[i] = after, i--);
1763
1697
  break;
1764
1698
  }
1765
1699
  contentLines.push(currentLine), i++;
@@ -1774,8 +1708,13 @@ const parseReviewFormat = (text, originalDoc) => {
1774
1708
  const contentLines = [];
1775
1709
  for (i++; i < lines.length; ) {
1776
1710
  const currentLine = lines[i];
1777
- if (currentLine.trim() === "</field>")
1711
+ if (currentLine.includes("</field>")) {
1712
+ const idx = currentLine.indexOf("</field>"), before = currentLine.substring(0, idx).trimEnd();
1713
+ before.trim() && contentLines.push(before);
1714
+ const after = currentLine.substring(idx + 8).trim();
1715
+ after && (lines[i] = after, i--);
1778
1716
  break;
1717
+ }
1779
1718
  contentLines.push(currentLine), i++;
1780
1719
  }
1781
1720
  if (node.text = contentLines.join(`
@@ -1788,32 +1727,36 @@ const parseReviewFormat = (text, originalDoc) => {
1788
1727
  stack.push(node);
1789
1728
  continue;
1790
1729
  }
1791
- const closeMatch = trimmed.match(/^<\/(field|block|span)>/);
1792
- if (closeMatch) {
1793
- if (stack.length === 0) {
1794
- devLog.warn("Closing tag found but stack is empty", {
1730
+ if (trimmed.match(/^<\/(field|block|span)>/)) {
1731
+ let afterClosingTag = trimmed;
1732
+ for (; ; ) {
1733
+ const match = afterClosingTag.match(/^<\/(field|block|span)>/);
1734
+ if (!match) break;
1735
+ if (stack.length === 0) {
1736
+ devLog.warn("Closing tag found but stack is empty", {
1737
+ line: i,
1738
+ tagName: match[1],
1739
+ trimmed: afterClosingTag
1740
+ }), afterClosingTag = afterClosingTag.substring(match[0].length).trim(), afterClosingTag = afterClosingTag.replace(/<!--[\s\S]*?-->/g, "").trim();
1741
+ continue;
1742
+ }
1743
+ matchedPatternCount++, devLog.debug("Matched closing tag", {
1795
1744
  line: i,
1796
- tagName: closeMatch[1],
1797
- trimmed
1745
+ tagName: match[1],
1746
+ trimmed: afterClosingTag,
1747
+ stackLength: stack.length,
1748
+ stackTopName: stack[stack.length - 1]?.name
1798
1749
  });
1799
- continue;
1750
+ const node = stack.pop();
1751
+ if (node)
1752
+ if (stack.length > 0) {
1753
+ const parent = stack[stack.length - 1];
1754
+ parent && parent.children.push(node);
1755
+ } else
1756
+ root.push(node);
1757
+ afterClosingTag = afterClosingTag.substring(match[0].length).trim(), afterClosingTag = afterClosingTag.replace(/<!--[\s\S]*?-->/g, "").trim();
1800
1758
  }
1801
- matchedPatternCount++, devLog.debug("Matched closing tag", {
1802
- line: i,
1803
- tagName: closeMatch[1],
1804
- trimmed,
1805
- stackLength: stack.length,
1806
- stackTopName: stack[stack.length - 1]?.name
1807
- });
1808
- const node = stack.pop();
1809
- if (node)
1810
- if (stack.length > 0) {
1811
- const parent = stack[stack.length - 1];
1812
- parent && parent.children.push(node);
1813
- } else
1814
- root.push(node);
1815
- let afterClosingTag = trimmed.substring(closeMatch[0].length).trim();
1816
- if (afterClosingTag = afterClosingTag.replace(/<!--[\s\S]*?-->/g, "").trim(), afterClosingTag) {
1759
+ if (afterClosingTag) {
1817
1760
  const remainingOpenMatch = afterClosingTag.match(/^<(field|block|span)([^>]*)>(.*)$/);
1818
1761
  if (remainingOpenMatch) {
1819
1762
  const [, tagName, attrsStr, content] = remainingOpenMatch, attributes = parseAttributes(attrsStr), remainingNode = {
@@ -1823,13 +1766,17 @@ const parseReviewFormat = (text, originalDoc) => {
1823
1766
  attributes,
1824
1767
  children: []
1825
1768
  }, remainingClosingTag = `</${tagName}>`;
1826
- if (content.includes(remainingClosingTag))
1827
- if (remainingNode.text = unescapeXml(content.replace(remainingClosingTag, "")), stack.length > 0) {
1769
+ if (content.includes(remainingClosingTag)) {
1770
+ const closeIdx = content.indexOf(remainingClosingTag);
1771
+ remainingNode.text = unescapeXml(content.substring(0, closeIdx));
1772
+ const remainder = content.substring(closeIdx + remainingClosingTag.length).trim();
1773
+ if (stack.length > 0) {
1828
1774
  const parent = stack[stack.length - 1];
1829
1775
  parent && parent.children.push(remainingNode);
1830
1776
  } else
1831
1777
  root.push(remainingNode);
1832
- else if (attributes.type === "string" && content)
1778
+ remainder && (lines[i] = remainder, i--);
1779
+ } else if (attributes.type === "string" && content)
1833
1780
  if (remainingNode.text = unescapeXml(content), stack.length > 0) {
1834
1781
  const parent = stack[stack.length - 1];
1835
1782
  parent && parent.children.push(remainingNode);
@@ -1839,8 +1786,13 @@ const parseReviewFormat = (text, originalDoc) => {
1839
1786
  const contentLines = [];
1840
1787
  for (i++; i < lines.length; ) {
1841
1788
  const currentLine = lines[i];
1842
- if (currentLine.trim() === `</${tagName}>` || currentLine.trim().startsWith(`</${tagName}>`))
1789
+ if (currentLine.includes(`</${tagName}>`)) {
1790
+ const idx = currentLine.indexOf(`</${tagName}>`), before = currentLine.substring(0, idx).trimEnd();
1791
+ before.trim() && contentLines.push(before);
1792
+ const after = currentLine.substring(idx + `</${tagName}>`.length).trim();
1793
+ after && (lines[i] = after, i--);
1843
1794
  break;
1795
+ }
1844
1796
  contentLines.push(currentLine), i++;
1845
1797
  }
1846
1798
  if (remainingNode.text = contentLines.join(`
@@ -1851,7 +1803,8 @@ const parseReviewFormat = (text, originalDoc) => {
1851
1803
  root.push(remainingNode);
1852
1804
  } else
1853
1805
  stack.push(remainingNode);
1854
- }
1806
+ } else
1807
+ lines[i] = afterClosingTag, i--;
1855
1808
  }
1856
1809
  continue;
1857
1810
  }
@@ -1968,22 +1921,10 @@ const parseReviewFormat = (text, originalDoc) => {
1968
1921
  });
1969
1922
  return;
1970
1923
  }
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
- }
1924
+ 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", {
1925
+ path: path.join("."),
1926
+ attemptedValue: node.text
1927
+ }));
1987
1928
  }, applyBlockChanges = (block, blockNode) => {
1988
1929
  if (blockNode.attributes._key && block._key !== blockNode.attributes._key) {
1989
1930
  devLog.warn("Block key mismatch, skipping", {