@saykit/transform-jsx 0.7.0 → 0.9.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 +37 -60
  2. package/package.json +3 -3
package/dist/parser.mjs CHANGED
@@ -2,11 +2,6 @@ import { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMes
2
2
  import { unwrapPlaceholder } from "@saykit/transform-js/parser";
3
3
  import * as t from "@babel/types";
4
4
  //#region src/parser.ts
5
- /**
6
- * Attribute used to name the ICU tag an embedded element extracts as, e.g.
7
- * `<a say-tag="link">` becomes `<link></link>` rather than `<0></0>`. It never
8
- * reaches the real element.
9
- */
10
5
  const TAG_ATTRIBUTE = "say-tag";
11
6
  const TAG_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
12
7
  function parseJSXContainerElement(element) {
@@ -14,21 +9,7 @@ function parseJSXContainerElement(element) {
14
9
  const processed = processJSXOpeningElement(element.openingElement);
15
10
  if (!processed) return null;
16
11
  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
- }, []);
12
+ const children = buildMessageChildren(element);
32
13
  const descriptorId = findAttributeValueIfStringLiteralAsString(element.openingElement.attributes, "id");
33
14
  const descriptorContext = findAttributeValueIfStringLiteralAsString(element.openingElement.attributes, "context");
34
15
  const whitespace = findAttributeValueAsBoolean(element.openingElement.attributes, "whitespace");
@@ -69,11 +50,13 @@ function parseJSXOpeningElement(element) {
69
50
  identifier,
70
51
  value: parseJSXElement(a.value.expression, true)
71
52
  });
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)) {
53
+ else if (t.isJSXFragment(a.value.expression)) {
54
+ const fragment = a.value.expression;
55
+ b.push({
56
+ identifier,
57
+ value: new CompositeMessage({}, [], [], buildMessageChildren(fragment), fragment)
58
+ });
59
+ } else if (t.isExpression(a.value.expression)) {
77
60
  const [name, value] = unwrapPlaceholder(a.value.expression);
78
61
  b.push({
79
62
  identifier,
@@ -102,11 +85,6 @@ function parseJSXOpeningElement(element) {
102
85
  }
103
86
  return null;
104
87
  }
105
- /**
106
- * The `offset` a plural subtracts from its selector before `#` is formatted.
107
- * See the JS parser's counterpart — the constraints are the same, only the
108
- * syntax carrying it differs.
109
- */
110
88
  function findPluralOffset(attributes) {
111
89
  for (const attribute of attributes) {
112
90
  if (!t.isJSXAttribute(attribute)) continue;
@@ -118,30 +96,35 @@ function findPluralOffset(attributes) {
118
96
  return expression.value;
119
97
  }
120
98
  }
121
- /**
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.
125
- */
126
- const CLOSING_PUNCTUATION = /^[.,;:!?)\]}»”’]/;
127
- /**
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.
134
- *
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.
138
- */
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;
99
+ function buildMessageChildren(element, into = []) {
100
+ return t.react.buildChildren(element).reduce((p, c) => {
101
+ const literal = getExpressionAsLiteralText(c);
102
+ if (literal !== void 0) pushLiteral(p, literal);
103
+ else if (t.isJSXElement(c)) p.push(parseJSXElement(c, true));
104
+ else if (t.isJSXFragment(c)) buildMessageChildren(c, p);
105
+ else if (t.isExpression(c)) {
106
+ const [identifier, value] = unwrapPlaceholder(c);
107
+ p.push(new ArgumentMessage(identifier, value));
108
+ }
109
+ return p;
110
+ }, into);
111
+ }
112
+ function pushLiteral(messages, text) {
113
+ if (!text) return;
114
+ const last = messages.at(-1);
115
+ if (last instanceof LiteralMessage) messages[messages.length - 1] = new LiteralMessage(last.text + text);
116
+ else messages.push(new LiteralMessage(text));
117
+ }
118
+ function getExpressionAsLiteralText(expression) {
119
+ if (t.isStringLiteral(expression)) return expression.value;
120
+ if (t.isNumericLiteral(expression)) return String(expression.value);
121
+ if (t.isUnaryExpression(expression) && (expression.operator === "-" || expression.operator === "+") && t.isNumericLiteral(expression.argument)) return String(expression.operator === "-" ? -expression.argument.value : expression.argument.value);
122
+ if (t.isBooleanLiteral(expression) || t.isNullLiteral(expression)) return "";
123
+ if (t.isTemplateLiteral(expression) && expression.expressions.length === 0) {
124
+ const [chunk] = expression.quasis;
125
+ /* v8 ignore next */
126
+ return chunk?.value.cooked ?? "";
127
+ }
145
128
  }
146
129
  function processJSXOpeningElement(element) {
147
130
  if (t.isJSXIdentifier(element.name) && element.name.name === "Say") return [element.name, null];
@@ -156,12 +139,6 @@ function parseJSXElement(element, fallback) {
156
139
  if (element.openingElement.selfClosing) return new ElementMessage(identifier, [], element);
157
140
  return new ElementMessage(identifier, [parseJSXElement(t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier("Say"), []), t.jsxClosingElement(t.jsxIdentifier("Say")), element.children), true)], element);
158
141
  }
159
- /**
160
- * Read the tag name off an embedded element and remove the attribute so it
161
- * never reaches the rendered element. Only static strings name a tag; anything
162
- * else is left in place and the element falls back to an auto-incremented
163
- * identifier.
164
- */
165
142
  function takeElementTag(element) {
166
143
  const attributes = element.openingElement.attributes;
167
144
  const index = attributes.findIndex((a) => t.isJSXAttribute(a) && getAttributeNameAsString(a) === TAG_ATTRIBUTE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saykit/transform-jsx",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.9.0",
51
+ "@saykit/transform-js": "^0.9.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@babel/core": "^7.29.7",