@saykit/transform-jsx 0.7.0 → 0.8.0

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.
Files changed (2) hide show
  1. package/dist/parser.mjs +54 -39
  2. package/package.json +3 -3
package/dist/parser.mjs CHANGED
@@ -14,21 +14,7 @@ function parseJSXContainerElement(element) {
14
14
  const processed = processJSXOpeningElement(element.openingElement);
15
15
  if (!processed) return null;
16
16
  const [accessor] = processed;
17
- const children = element.children.reduce((p, c, i, a) => {
18
- if (t.isJSXText(c)) {
19
- const text = normalizeJSXText(c.value, i === 0, i === a.length - 1);
20
- if (text) p.push(new LiteralMessage(text));
21
- }
22
- if (t.isJSXElement(c)) p.push(parseJSXElement(c, true));
23
- else if (t.isJSXFragment(c)) p.push(new ElementMessage(AUTO_INCREMENT_IDENTIFIER, [], c));
24
- else if (t.isJSXExpressionContainer(c)) {
25
- if (t.isExpression(c.expression)) {
26
- const [identifier, value] = unwrapPlaceholder(c.expression);
27
- p.push(new ArgumentMessage(identifier, value));
28
- }
29
- }
30
- return p;
31
- }, []);
17
+ const children = buildMessageChildren(element);
32
18
  const descriptorId = findAttributeValueIfStringLiteralAsString(element.openingElement.attributes, "id");
33
19
  const descriptorContext = findAttributeValueIfStringLiteralAsString(element.openingElement.attributes, "context");
34
20
  const whitespace = findAttributeValueAsBoolean(element.openingElement.attributes, "whitespace");
@@ -69,11 +55,13 @@ function parseJSXOpeningElement(element) {
69
55
  identifier,
70
56
  value: parseJSXElement(a.value.expression, true)
71
57
  });
72
- else if (t.isJSXFragment(a.value.expression)) b.push({
73
- identifier,
74
- value: new ElementMessage(AUTO_INCREMENT_IDENTIFIER, [], a.value.expression)
75
- });
76
- else if (t.isExpression(a.value.expression)) {
58
+ else if (t.isJSXFragment(a.value.expression)) {
59
+ const fragment = a.value.expression;
60
+ b.push({
61
+ identifier,
62
+ value: new CompositeMessage({}, [], [], buildMessageChildren(fragment), fragment)
63
+ });
64
+ } else if (t.isExpression(a.value.expression)) {
77
65
  const [name, value] = unwrapPlaceholder(a.value.expression);
78
66
  b.push({
79
67
  identifier,
@@ -119,29 +107,56 @@ function findPluralOffset(attributes) {
119
107
  }
120
108
  }
121
109
  /**
122
- * Punctuation that never takes a space in front of it. A formatter wrapping
123
- * long JSX regularly strands these at the start of a line, on their own or
124
- * ahead of the rest of a clause.
110
+ * The children the JSX transform itself would compile, so a message extracts as
111
+ * the text that renders. Text children come back with their whitespace
112
+ * collapsed the way JSX collapses it — a line break and the indentation around
113
+ * it are layout and disappear, while two lines that both hold text rejoin with
114
+ * the single space that separated the words — every expression container comes
115
+ * back unwrapped, and a comment child comes back as nothing.
125
116
  */
126
- const CLOSING_PUNCTUATION = /^[.,;:!?)\]}»”’]/;
117
+ function buildMessageChildren(element, into = []) {
118
+ return t.react.buildChildren(element).reduce((p, c) => {
119
+ const literal = getExpressionAsLiteralText(c);
120
+ if (literal !== void 0) pushLiteral(p, literal);
121
+ else if (t.isJSXElement(c)) p.push(parseJSXElement(c, true));
122
+ else if (t.isJSXFragment(c)) buildMessageChildren(c, p);
123
+ else if (t.isExpression(c)) {
124
+ const [identifier, value] = unwrapPlaceholder(c);
125
+ p.push(new ArgumentMessage(identifier, value));
126
+ }
127
+ return p;
128
+ }, into);
129
+ }
127
130
  /**
128
- * Collapse the whitespace in a JSX text child the way the source reads.
129
- *
130
- * A line break between a word and its neighbour is only how a formatter wrapped
131
- * a long line, so it still means a space. Dropping it would run the words
132
- * together — and because ids are content hashes, silently orphan every existing
133
- * translation for the message.
131
+ * Add text to the message, folding it into the literal in front of it when
132
+ * there is one, so `Hello{' '}world` is three children in the source and one
133
+ * run of text in the catalogue.
134
+ */
135
+ function pushLiteral(messages, text) {
136
+ if (!text) return;
137
+ const last = messages.at(-1);
138
+ if (last instanceof LiteralMessage) messages[messages.length - 1] = new LiteralMessage(last.text + text);
139
+ else messages.push(new LiteralMessage(text));
140
+ }
141
+ /**
142
+ * The text a child renders as, when that is knowable at build time — a text
143
+ * child, which `buildChildren` has already collapsed into a string, or an
144
+ * expression holding a literal with nothing interpolated into it.
134
145
  *
135
- * The exception is a line that begins with punctuation, which belongs tight
136
- * against whatever precedes it. Only an implicit break is treated this way: a
137
- * space the author wrote on the same line is deliberate and always survives.
146
+ * The second is what makes `{' '}` the way to write whitespace that has to
147
+ * survive a line break: it extracts as the space it renders as, rather than as
148
+ * a placeholder a translator can neither see nor move.
138
149
  */
139
- function normalizeJSXText(value, isFirst, isLast) {
140
- let text = value.replace(/\s+/g, " ");
141
- if (isFirst) text = text.trimStart();
142
- else if (text.startsWith(" ") && /^[^\S\n]*\n/.test(value) && CLOSING_PUNCTUATION.test(text.slice(1))) text = text.slice(1);
143
- if (isLast) text = text.trimEnd();
144
- return text;
150
+ function getExpressionAsLiteralText(expression) {
151
+ if (t.isStringLiteral(expression)) return expression.value;
152
+ if (t.isNumericLiteral(expression)) return String(expression.value);
153
+ if (t.isUnaryExpression(expression) && (expression.operator === "-" || expression.operator === "+") && t.isNumericLiteral(expression.argument)) return String(expression.operator === "-" ? -expression.argument.value : expression.argument.value);
154
+ if (t.isBooleanLiteral(expression) || t.isNullLiteral(expression)) return "";
155
+ if (t.isTemplateLiteral(expression) && expression.expressions.length === 0) {
156
+ const [chunk] = expression.quasis;
157
+ /* v8 ignore next */
158
+ return chunk?.value.cooked ?? "";
159
+ }
145
160
  }
146
161
  function processJSXOpeningElement(element) {
147
162
  if (t.isJSXIdentifier(element.name) && element.name.name === "Say") return [element.name, null];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saykit/transform-jsx",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "JSX and TSX source transformer for SayKit",
5
5
  "keywords": [
6
6
  "i18n",
@@ -47,8 +47,8 @@
47
47
  "@babel/parser": "^7.29.7",
48
48
  "@babel/traverse": "^7.29.7",
49
49
  "@babel/types": "^7.29.7",
50
- "@saykit/config": "^0.7.0",
51
- "@saykit/transform-js": "^0.7.0"
50
+ "@saykit/config": "^0.8.0",
51
+ "@saykit/transform-js": "^0.8.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@babel/core": "^7.29.7",