react-native-text-measure 1.0.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.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # react-native-text-measure
2
+
3
+ > Accurately measure text `width`, `height`, and `lineCount` using native layout engines — no view rendered.
4
+
5
+ | Platform | Engine | Accuracy |
6
+ |----------|--------|----------|
7
+ | iOS | `NSLayoutManager` (TextKit) | ✅ Identical to `UILabel` |
8
+ | Android | `StaticLayout` | ✅ Identical to `TextView` |
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ ```sh
15
+ npm install react-native-text-measure
16
+ # or
17
+ yarn add react-native-text-measure
18
+ ```
19
+
20
+ ### iOS
21
+ ```sh
22
+ cd ios && pod install
23
+ ```
24
+
25
+ ### Android
26
+ Auto-linking handles it on RN >= 0.60. For manual linking see [`android/README.md`](android/README.md).
27
+
28
+ ---
29
+
30
+ ## Usage
31
+
32
+ ```js
33
+ import { measureText, measureTextSync } from 'react-native-text-measure';
34
+
35
+ // ── Async (recommended) ───────────────────────────────────────────────────────
36
+ const { width, height, lineCount } = await measureText('Hello world', {
37
+ fontSize: 20,
38
+ fontFamily: 'Helvetica',
39
+ fontWeight: 'normal',
40
+ maxWidth: 300,
41
+ });
42
+
43
+ // ── Sync (use sparingly — blocks JS thread) ───────────────────────────────────
44
+ const result = measureTextSync('Hello', { fontSize: 16 });
45
+ ```
46
+
47
+ ---
48
+
49
+ ## API
50
+
51
+ ### `measureText(text, options)` → `Promise<Result>`
52
+ ### `measureTextSync(text, options)` → `Result`
53
+
54
+ #### Options
55
+
56
+ | Option | Type | Default | Description |
57
+ |--------|------|---------|-------------|
58
+ | `fontSize` | `number` | `14` | Font size in dp/pt |
59
+ | `fontFamily` | `string` | system | Font family name |
60
+ | `fontWeight` | `string` | `'normal'` | `'normal'`, `'bold'`, `'100'`–`'900'` |
61
+ | `fontStyle` | `string` | `'normal'` | `'normal'` or `'italic'` |
62
+ | `letterSpacing` | `number` | `0` | Extra letter spacing in dp/pt |
63
+ | `lineHeight` | `number` | `0` | Explicit line height. `0` = natural |
64
+ | `maxWidth` | `number` | `0` | Wrap width. `0` = no wrap (single line) |
65
+ | `maxHeight` | `number` | `0` | Clamp returned height. `0` = unclamped |
66
+ | `numberOfLines` | `number` | `0` | Max lines. `0` = unlimited |
67
+ | `includeFontPadding` | `boolean` | `true` | **Android only.** Match your `<Text>` setting |
68
+
69
+ #### Result
70
+
71
+ ```ts
72
+ {
73
+ width: number; // dp (Android) / pt (iOS)
74
+ height: number; // dp (Android) / pt (iOS)
75
+ lineCount: number;
76
+ }
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Accuracy notes
82
+
83
+ - Results are **not estimates** — both engines perform a full layout pass.
84
+ - `letterSpacing` unit differs between platforms (see inline comments).
85
+ - `includeFontPadding` must match your `<Text>` component on Android.
86
+ - If `fontFamily` is not found, the system font is used silently — measurement will be accurate but for the wrong font.
87
+
88
+ ---
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,14 @@
1
+ README
2
+ ======
3
+
4
+ If you want to publish the lib as a maven dependency, follow these steps before publishing a new version to npm:
5
+
6
+ 1. Be sure to have the Android [SDK](https://developer.android.com/studio/index.html) and [NDK](https://developer.android.com/ndk/guides/index.html) installed
7
+ 2. Be sure to have a `local.properties` file in this folder that points to the Android SDK and NDK
8
+ ```
9
+ ndk.dir=/Users/{username}/Library/Android/sdk/ndk-bundle
10
+ sdk.dir=/Users/{username}/Library/Android/sdk
11
+ ```
12
+ 3. Delete the `maven` folder
13
+ 4. Run `./gradlew installArchives`
14
+ 5. Verify that latest set of generated files is in the maven folder with the correct version number
@@ -0,0 +1,148 @@
1
+ // android/build.gradle
2
+
3
+ // based on:
4
+ //
5
+ // * https://github.com/facebook/react-native/blob/0.60-stable/template/android/build.gradle
6
+ // previous location:
7
+ // - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/build.gradle
8
+ //
9
+ // * https://github.com/facebook/react-native/blob/0.60-stable/template/android/app/build.gradle
10
+ // previous location:
11
+ // - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/app/build.gradle
12
+
13
+ // These defaults should reflect the SDK versions used by
14
+ // the minimum React Native version supported.
15
+ def DEFAULT_COMPILE_SDK_VERSION = 28
16
+ def DEFAULT_BUILD_TOOLS_VERSION = '28.0.3'
17
+ def DEFAULT_MIN_SDK_VERSION = 16
18
+ def DEFAULT_TARGET_SDK_VERSION = 28
19
+
20
+ def safeExtGet(prop, fallback) {
21
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
22
+ }
23
+
24
+ apply plugin: 'com.android.library'
25
+ apply plugin: 'maven'
26
+
27
+ buildscript {
28
+ // The Android Gradle plugin is only required when opening the android folder stand-alone.
29
+ // This avoids unnecessary downloads and potential conflicts when the library is included as a
30
+ // module dependency in an application project.
31
+ // ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies
32
+ if (project == rootProject) {
33
+ repositories {
34
+ google()
35
+ }
36
+ dependencies {
37
+ // This should reflect the Gradle plugin version used by
38
+ // the minimum React Native version supported.
39
+ classpath 'com.android.tools.build:gradle:3.4.1'
40
+ }
41
+ }
42
+ }
43
+
44
+ android {
45
+ compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
46
+ buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
47
+ defaultConfig {
48
+ minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION)
49
+ targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
50
+ versionCode 1
51
+ versionName "1.0"
52
+ }
53
+ lintOptions {
54
+ abortOnError false
55
+ }
56
+ }
57
+
58
+ repositories {
59
+ // ref: https://www.baeldung.com/maven-local-repository
60
+ mavenLocal()
61
+ maven {
62
+ // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
63
+ url "$rootDir/../node_modules/react-native/android"
64
+ }
65
+ maven {
66
+ // Android JSC is installed from npm
67
+ url "$rootDir/../node_modules/jsc-android/dist"
68
+ }
69
+ google()
70
+ }
71
+
72
+ dependencies {
73
+ //noinspection GradleDynamicVersion
74
+ implementation 'com.facebook.react:react-native:+' // From node_modules
75
+ }
76
+
77
+ def configureReactNativePom(def pom) {
78
+ def packageJson = new groovy.json.JsonSlurper().parseText(file('../package.json').text)
79
+
80
+ pom.project {
81
+ name packageJson.title
82
+ artifactId packageJson.name
83
+ version = packageJson.version
84
+ group = "com.reactlibrary"
85
+ description packageJson.description
86
+ url packageJson.repository.baseUrl
87
+
88
+ licenses {
89
+ license {
90
+ name packageJson.license
91
+ url packageJson.repository.baseUrl + '/blob/master/' + packageJson.licenseFilename
92
+ distribution 'repo'
93
+ }
94
+ }
95
+
96
+ developers {
97
+ developer {
98
+ id packageJson.author.username
99
+ name packageJson.author.name
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ afterEvaluate { project ->
106
+ // some Gradle build hooks ref:
107
+ // https://www.oreilly.com/library/view/gradle-beyond-the/9781449373801/ch03.html
108
+ task androidJavadoc(type: Javadoc) {
109
+ source = android.sourceSets.main.java.srcDirs
110
+ classpath += files(android.bootClasspath)
111
+ classpath += files(project.getConfigurations().getByName('compile').asList())
112
+ include '**/*.java'
113
+ }
114
+
115
+ task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
116
+ classifier = 'javadoc'
117
+ from androidJavadoc.destinationDir
118
+ }
119
+
120
+ task androidSourcesJar(type: Jar) {
121
+ classifier = 'sources'
122
+ from android.sourceSets.main.java.srcDirs
123
+ include '**/*.java'
124
+ }
125
+
126
+ android.libraryVariants.all { variant ->
127
+ def name = variant.name.capitalize()
128
+ def javaCompileTask = variant.javaCompileProvider.get()
129
+
130
+ task "jar${name}"(type: Jar, dependsOn: javaCompileTask) {
131
+ from javaCompileTask.destinationDir
132
+ }
133
+ }
134
+
135
+ artifacts {
136
+ archives androidSourcesJar
137
+ archives androidJavadocJar
138
+ }
139
+
140
+ task installArchives(type: Upload) {
141
+ configuration = configurations.archives
142
+ repositories.mavenDeployer {
143
+ // Deploy to react-native-event-bridge/maven, ready to publish to npm
144
+ repository url: "file://${projectDir}/../android/maven"
145
+ configureReactNativePom pom
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,6 @@
1
+ <!-- AndroidManifest.xml -->
2
+
3
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
4
+ package="com.reactlibrary">
5
+
6
+ </manifest>
@@ -0,0 +1,292 @@
1
+ package com.reactlibrary;
2
+
3
+ import android.graphics.Paint;
4
+ import android.graphics.Typeface;
5
+ import android.os.Build;
6
+ import android.text.Layout;
7
+ import android.text.StaticLayout;
8
+ import android.text.TextPaint;
9
+ import android.text.TextUtils;
10
+
11
+ import com.facebook.react.bridge.Arguments;
12
+ import com.facebook.react.bridge.Promise;
13
+ import com.facebook.react.bridge.ReactApplicationContext;
14
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
15
+ import com.facebook.react.bridge.ReactMethod;
16
+ import com.facebook.react.bridge.ReadableMap;
17
+ import com.facebook.react.bridge.WritableMap;
18
+
19
+ import androidx.annotation.NonNull;
20
+ import androidx.annotation.Nullable;
21
+
22
+ /**
23
+ * ReactNativeTextMeasureModule
24
+ *
25
+ * Measures text bounding dimensions (width, height, lineCount) using Android's
26
+ * StaticLayout — the same engine that TextView uses. No View is created or rendered.
27
+ *
28
+ * All input values are in density-independent pixels (dp).
29
+ * All output values are also in dp, matching React Native's coordinate system.
30
+ *
31
+ * JS API:
32
+ * measureText(text, options) → Promise<{ width, height, lineCount }>
33
+ * measureTextSync(text, options) → { width, height, lineCount }
34
+ */
35
+ public class ReactNativeTextMeasureModule extends ReactContextBaseJavaModule {
36
+
37
+ private final ReactApplicationContext reactContext;
38
+
39
+ public ReactNativeTextMeasureModule(ReactApplicationContext context) {
40
+ super(context);
41
+ this.reactContext = context;
42
+ }
43
+
44
+ @NonNull
45
+ @Override
46
+ public String getName() {
47
+ // Must match NativeModules.ReactNativeTextMeasure on the JS side
48
+ return "ReactNativeTextMeasure";
49
+ }
50
+
51
+ // ─────────────────────────────────────────────────────────────────────────
52
+ // JS-callable: async (recommended)
53
+ // ─────────────────────────────────────────────────────────────────────────
54
+
55
+ /**
56
+ * Async measurement. Resolves with { width, height, lineCount }.
57
+ * Runs on the React Native background thread pool — does not block JS.
58
+ */
59
+ @ReactMethod
60
+ public void measureText(String text, ReadableMap options, Promise promise) {
61
+ try {
62
+ promise.resolve(performMeasure(text, options));
63
+ } catch (Exception e) {
64
+ promise.reject("MEASURE_ERROR", e.getMessage(), e);
65
+ }
66
+ }
67
+
68
+ // ─────────────────────────────────────────────────────────────────────────
69
+ // JS-callable: synchronous
70
+ // ─────────────────────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Synchronous measurement. Returns { width, height, lineCount } directly.
74
+ *
75
+ * ⚠️ Runs on the JS thread. Use only when async is not an option.
76
+ * For old architecture: isBlockingSynchronousMethod = true is required.
77
+ * For new architecture (TurboModules): implement NativeReactNativeTextMeasureSpec.
78
+ */
79
+ @ReactMethod(isBlockingSynchronousMethod = true)
80
+ public WritableMap measureTextSync(String text, ReadableMap options) {
81
+ try {
82
+ return performMeasure(text, options);
83
+ } catch (Exception e) {
84
+ WritableMap error = Arguments.createMap();
85
+ error.putString("error", e.getMessage());
86
+ return error;
87
+ }
88
+ }
89
+
90
+ // ─────────────────────────────────────────────────────────────────────────
91
+ // Core measurement — called by both async and sync entry points
92
+ // ─────────────────────────────────────────────────────────────────────────
93
+
94
+ private WritableMap performMeasure(@Nullable String text, @Nullable ReadableMap options) {
95
+ if (text == null) text = "";
96
+ if (options == null) options = Arguments.createMap();
97
+
98
+ // ── 1. Parse options ─────────────────────────────────────────────────
99
+ float fontSize = getFloat(options, "fontSize", 14f);
100
+ String fontFamily = getString(options, "fontFamily", null);
101
+ String fontWeight = getString(options, "fontWeight", "normal");
102
+ String fontStyle = getString(options, "fontStyle", "normal");
103
+ float letterSpacing = getFloat(options, "letterSpacing", 0f);
104
+ float lineHeight = getFloat(options, "lineHeight", 0f);
105
+ float maxWidth = getFloat(options, "maxWidth", 0f);
106
+ float maxHeight = getFloat(options, "maxHeight", 0f);
107
+ int numberOfLines = getInt(options, "numberOfLines", 0);
108
+ boolean includeFontPad = getBool(options, "includeFontPadding", true);
109
+
110
+ // ── 2. dp → px conversion ─────────────────────────────────────────────
111
+ // React Native uses dp everywhere; Android Paint/StaticLayout needs px.
112
+ float density = reactContext.getResources().getDisplayMetrics().density;
113
+ float fontSizePx = fontSize * density;
114
+ float lineHeightPx = lineHeight * density;
115
+ int maxWidthPx = (maxWidth > 0) ? Math.round(maxWidth * density) : Integer.MAX_VALUE;
116
+ int maxHeightPx = (maxHeight > 0) ? Math.round(maxHeight * density) : Integer.MAX_VALUE;
117
+
118
+ // ── 3. Resolve Typeface ───────────────────────────────────────────────
119
+ boolean isBold = "bold".equalsIgnoreCase(fontWeight) || numericWeightIsBold(fontWeight);
120
+ boolean isItalic = "italic".equalsIgnoreCase(fontStyle);
121
+
122
+ int typefaceStyle;
123
+ if (isBold && isItalic) typefaceStyle = Typeface.BOLD_ITALIC;
124
+ else if (isBold) typefaceStyle = Typeface.BOLD;
125
+ else if (isItalic) typefaceStyle = Typeface.ITALIC;
126
+ else typefaceStyle = Typeface.NORMAL;
127
+
128
+ Typeface typeface = resolveTypeface(fontFamily, typefaceStyle);
129
+
130
+ // ── 4. Build TextPaint ────────────────────────────────────────────────
131
+ TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
132
+ paint.setTextSize(fontSizePx);
133
+ paint.setTypeface(typeface);
134
+
135
+ // letterSpacing: Android uses EM fractions; convert from dp.
136
+ // letterSpacing_em = letterSpacing_px / fontSizePx
137
+ // We treat the user's value as dp, so first dp→px, then →em.
138
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && letterSpacing != 0f) {
139
+ float letterSpacingPx = letterSpacing * density;
140
+ paint.setLetterSpacing(letterSpacingPx / fontSizePx);
141
+ }
142
+
143
+ // ── 5. Build StaticLayout ─────────────────────────────────────────────
144
+ //
145
+ // StaticLayout is the exact engine TextView uses for static (non-editable) text.
146
+ // It performs full Unicode line-breaking, glyph shaping, and produces the real
147
+ // pixel-accurate dimensions — not an estimate.
148
+ //
149
+ StaticLayout layout = buildStaticLayout(
150
+ text, paint, maxWidthPx,
151
+ lineHeightPx, numberOfLines, includeFontPad
152
+ );
153
+
154
+ // ── 6. Extract dimensions ─────────────────────────────────────────────
155
+ int lineCount = layout.getLineCount();
156
+ float measuredHeight = layout.getHeight();
157
+ float measuredWidth = 0f;
158
+
159
+ for (int i = 0; i < lineCount; i++) {
160
+ float lineWidth = layout.getLineWidth(i);
161
+ if (lineWidth > measuredWidth) measuredWidth = lineWidth;
162
+ }
163
+
164
+ // Clamp height if maxHeight was provided
165
+ if (maxHeightPx != Integer.MAX_VALUE && measuredHeight > maxHeightPx) {
166
+ measuredHeight = maxHeightPx;
167
+ }
168
+
169
+ // ── 7. px → dp for output ─────────────────────────────────────────────
170
+ WritableMap result = Arguments.createMap();
171
+ result.putDouble("width", measuredWidth / density);
172
+ result.putDouble("height", measuredHeight / density);
173
+ result.putInt( "lineCount", lineCount);
174
+ return result;
175
+ }
176
+
177
+ // ─────────────────────────────────────────────────────────────────────────
178
+ // StaticLayout builder — handles API version differences
179
+ // ─────────────────────────────────────────────────────────────────────────
180
+
181
+ @SuppressWarnings("deprecation")
182
+ private StaticLayout buildStaticLayout(
183
+ String text,
184
+ TextPaint paint,
185
+ int maxWidthPx,
186
+ float lineHeightPx,
187
+ int numberOfLines,
188
+ boolean includeFontPad
189
+ ) {
190
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
191
+ // API 23+ — use the Builder (clean, full-featured)
192
+ StaticLayout.Builder builder = StaticLayout.Builder
193
+ .obtain(text, 0, text.length(), paint, maxWidthPx)
194
+ .setAlignment(Layout.Alignment.ALIGN_NORMAL)
195
+ .setIncludePad(includeFontPad)
196
+ .setMaxLines(numberOfLines > 0 ? numberOfLines : Integer.MAX_VALUE)
197
+ .setEllipsize(numberOfLines > 0 ? TextUtils.TruncateAt.END : null);
198
+
199
+ if (lineHeightPx > 0) {
200
+ // Android line spacing: total_line_height = naturalLineHeight * mult + add
201
+ // We want total = lineHeightPx, so: add = lineHeightPx - natural, mult = 1
202
+ float naturalLineHeight = -paint.ascent() + paint.descent();
203
+ builder.setLineSpacing(lineHeightPx - naturalLineHeight, 1f);
204
+ } else {
205
+ builder.setLineSpacing(0f, 1f);
206
+ }
207
+
208
+ return builder.build();
209
+
210
+ } else {
211
+ // API 21–22 fallback — deprecated constructor but still accurate
212
+ float spacingAdd = 0f;
213
+ float spacingMult = 1f;
214
+
215
+ if (lineHeightPx > 0) {
216
+ float naturalLineHeight = -paint.ascent() + paint.descent();
217
+ spacingAdd = lineHeightPx - naturalLineHeight;
218
+ }
219
+
220
+ return new StaticLayout(
221
+ text, 0, text.length(),
222
+ paint, maxWidthPx,
223
+ Layout.Alignment.ALIGN_NORMAL,
224
+ spacingMult, spacingAdd,
225
+ includeFontPad,
226
+ numberOfLines > 0 ? TextUtils.TruncateAt.END : null,
227
+ maxWidthPx
228
+ );
229
+ }
230
+ }
231
+
232
+ // ─────────────────────────────────────────────────────────────────────────
233
+ // Typeface resolution
234
+ // ─────────────────────────────────────────────────────────────────────────
235
+
236
+ private Typeface resolveTypeface(@Nullable String fontFamily, int style) {
237
+ if (fontFamily != null && !fontFamily.isEmpty()) {
238
+ // 1. Try assets/fonts/<fontFamily>.ttf (common RN convention)
239
+ try {
240
+ Typeface fromAssets = Typeface.createFromAsset(
241
+ reactContext.getAssets(),
242
+ "fonts/" + fontFamily + ".ttf"
243
+ );
244
+ if (fromAssets != null) {
245
+ return Typeface.create(fromAssets, style);
246
+ }
247
+ } catch (Exception ignored) { /* not in assets */ }
248
+
249
+ // 2. Try as a system font name (e.g. "sans-serif", "monospace", "Roboto")
250
+ try {
251
+ Typeface systemFont = Typeface.create(fontFamily, style);
252
+ if (systemFont != null) return systemFont;
253
+ } catch (Exception ignored) { /* unknown family */ }
254
+ }
255
+
256
+ // 3. Default system font with the requested style
257
+ return Typeface.create((String) null, style);
258
+ }
259
+
260
+ // ─────────────────────────────────────────────────────────────────────────
261
+ // ReadableMap helpers (null-safe)
262
+ // ─────────────────────────────────────────────────────────────────────────
263
+
264
+ private float getFloat(ReadableMap map, String key, float def) {
265
+ if (map.hasKey(key) && !map.isNull(key)) return (float) map.getDouble(key);
266
+ return def;
267
+ }
268
+
269
+ private int getInt(ReadableMap map, String key, int def) {
270
+ if (map.hasKey(key) && !map.isNull(key)) return map.getInt(key);
271
+ return def;
272
+ }
273
+
274
+ private boolean getBool(ReadableMap map, String key, boolean def) {
275
+ if (map.hasKey(key) && !map.isNull(key)) return map.getBoolean(key);
276
+ return def;
277
+ }
278
+
279
+ @Nullable
280
+ private String getString(ReadableMap map, String key, @Nullable String def) {
281
+ if (map.hasKey(key) && !map.isNull(key)) return map.getString(key);
282
+ return def;
283
+ }
284
+
285
+ private boolean numericWeightIsBold(String weight) {
286
+ try {
287
+ return Integer.parseInt(weight.trim()) >= 600;
288
+ } catch (NumberFormatException e) {
289
+ return false;
290
+ }
291
+ }
292
+ }
@@ -0,0 +1,44 @@
1
+ package com.reactlibrary;
2
+
3
+ import com.facebook.react.ReactPackage;
4
+ import com.facebook.react.bridge.NativeModule;
5
+ import com.facebook.react.bridge.ReactApplicationContext;
6
+ import com.facebook.react.uimanager.ViewManager;
7
+
8
+ import java.util.Arrays;
9
+ import java.util.Collections;
10
+ import java.util.List;
11
+
12
+ /**
13
+ * ReactNativeTextMeasurePackage
14
+ *
15
+ * Registers ReactNativeTextMeasureModule with the React Native bridge.
16
+ *
17
+ * Add to your MainApplication.java:
18
+ *
19
+ * @Override
20
+ * protected List<ReactPackage> getPackages() {
21
+ * return Arrays.<ReactPackage>asList(
22
+ * new MainReactPackage(),
23
+ * new ReactNativeTextMeasurePackage() // ← add this line
24
+ * );
25
+ * }
26
+ *
27
+ * If you are on RN >= 0.60 with auto-linking enabled, you do NOT need to
28
+ * manually add the package — it will be registered automatically via
29
+ * react-native.config.js.
30
+ */
31
+ public class ReactNativeTextMeasurePackage implements ReactPackage {
32
+
33
+ @Override
34
+ public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
35
+ return Arrays.<NativeModule>asList(
36
+ new ReactNativeTextMeasureModule(reactContext)
37
+ );
38
+ }
39
+
40
+ @Override
41
+ public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
42
+ return Collections.emptyList();
43
+ }
44
+ }
package/index.js ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * react-native-text-measure
3
+ *
4
+ * Accurately measures text dimensions using native layout engines.
5
+ * iOS → NSLayoutManager (TextKit) — identical to UILabel
6
+ * Android → StaticLayout — identical to TextView
7
+ *
8
+ * No view is rendered. Measurement is pure native computation.
9
+ */
10
+
11
+ import { NativeModules } from "react-native";
12
+
13
+ const LINKING_ERROR =
14
+ `react-native-text-measure: Native module not found.\n\n` +
15
+ `iOS: Make sure you ran \`pod install\` and the .m file is in your Xcode project.\n` +
16
+ `Android: Make sure ReactNativeTextMeasurePackage is registered in MainApplication.\n\n` +
17
+ `If you are using React Native >= 0.60, auto-linking should handle this.\n` +
18
+ `Try rebuilding the app.`;
19
+
20
+ const NativeTextMeasure = NativeModules.ReactNativeTextMeasure
21
+ ? NativeModules.ReactNativeTextMeasure
22
+ : new Proxy(
23
+ {},
24
+ {
25
+ get() {
26
+ throw new Error(LINKING_ERROR);
27
+ },
28
+ },
29
+ );
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // JSDoc typedef (works in plain JS + IDEs; see index.d.ts for TypeScript)
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * @typedef {Object} TextMeasureOptions
37
+ * @property {number} [fontSize=14] - Font size in dp (Android) / pt (iOS).
38
+ * @property {string} [fontFamily] - Font family name. Falls back to system font.
39
+ * @property {string} [fontWeight='normal']- 'normal' | 'bold' | '100'–'900'.
40
+ * @property {string} [fontStyle='normal'] - 'normal' | 'italic'.
41
+ * @property {number} [letterSpacing=0] - Extra letter spacing in dp/pt.
42
+ * @property {number} [lineHeight=0] - Explicit line height. 0 = natural.
43
+ * @property {number} [maxWidth=0] - Wrap width. 0 = no wrapping.
44
+ * @property {number} [maxHeight=0] - Clamp height. 0 = unclamped.
45
+ * @property {number} [numberOfLines=0] - Max lines. 0 = unlimited.
46
+ * @property {boolean} [includeFontPadding=true] - Android only: match RN Text default.
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} TextMeasureResult
51
+ * @property {number} width - Measured width in dp (Android) / pt (iOS).
52
+ * @property {number} height - Measured height in dp (Android) / pt (iOS).
53
+ * @property {number} lineCount - Number of lines the text was laid out into.
54
+ */
55
+
56
+ // ─────────────────────────────────────────────────────────────────────────────
57
+ // Public API
58
+ // ─────────────────────────────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Asynchronously measures the bounding box of `text` with the given style options.
62
+ * Runs off the JS thread — safe to call frequently.
63
+ *
64
+ * @param {string} text
65
+ * @param {TextMeasureOptions} [options={}]
66
+ * @returns {Promise<TextMeasureResult>}
67
+ *
68
+ * @example
69
+ * const { width, height, lineCount } = await measureText('Hello world', {
70
+ * fontSize: 20,
71
+ * fontFamily: 'Helvetica',
72
+ * fontWeight: 'normal',
73
+ * maxWidth: 200,
74
+ * });
75
+ */
76
+ export function measureText(text, options = {}) {
77
+ return NativeTextMeasure.measureText(String(text), sanitize(options));
78
+ }
79
+
80
+ /**
81
+ * Synchronously measures text. Runs on the JS thread.
82
+ * ⚠️ Prefer `measureText` (async). Use this only when you cannot await
83
+ * (e.g., inside a synchronous layout calculation).
84
+ *
85
+ * @param {string} text
86
+ * @param {TextMeasureOptions} [options={}]
87
+ * @returns {TextMeasureResult}
88
+ *
89
+ * @example
90
+ * const { height } = measureTextSync('Hello', { fontSize: 16 });
91
+ */
92
+ export function measureTextSync(text, options = {}) {
93
+ return NativeTextMeasure.measureTextSync(String(text), sanitize(options));
94
+ }
95
+
96
+ // ─────────────────────────────────────────────────────────────────────────────
97
+ // Helpers
98
+ // ─────────────────────────────────────────────────────────────────────────────
99
+
100
+ /**
101
+ * Remove undefined keys so the native side receives a clean map.
102
+ * @param {TextMeasureOptions} options
103
+ * @returns {Object}
104
+ */
105
+ function sanitize(options) {
106
+ const out = {};
107
+ for (const [key, value] of Object.entries(options)) {
108
+ if (value !== undefined && value !== null) {
109
+ out[key] = value;
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+
115
+ export default { measureText, measureTextSync };
@@ -0,0 +1,19 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /**
6
+ * ReactNativeTextMeasure
7
+ *
8
+ * Native module that measures text bounding dimensions (width, height, lineCount)
9
+ * using Apple's TextKit layout engine — identical to UILabel / UITextView rendering.
10
+ * No view is created or rendered.
11
+ *
12
+ * Exposes two JS methods:
13
+ * measureText(text, options) → Promise (async, background thread)
14
+ * measureTextSync(text, options) → Object (sync, JS thread)
15
+ */
16
+ @interface ReactNativeTextMeasure : NSObject <RCTBridgeModule>
17
+ @end
18
+
19
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,204 @@
1
+ #import "ReactNativeTextMeasure.h"
2
+ #import <UIKit/UIKit.h>
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Private helper — builds the measurement result dict from a string + options.
6
+ // Extracted so both the async and sync entry-points share identical logic.
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ static NSDictionary *RNTMPerformMeasure(NSString *text, NSDictionary *options) {
9
+
10
+ // ── 1. Parse options ───────────────────────────────────────────────────────
11
+ CGFloat fontSize = options[@"fontSize"] ? [options[@"fontSize"] doubleValue] : 14.0;
12
+ NSString *fontFamily = options[@"fontFamily"];
13
+ NSString *fontWeight = options[@"fontWeight"] ?: @"normal";
14
+ NSString *fontStyle = options[@"fontStyle"] ?: @"normal";
15
+ CGFloat letterSpacing = options[@"letterSpacing"] ? [options[@"letterSpacing"] doubleValue] : 0.0;
16
+ CGFloat lineHeight = options[@"lineHeight"] ? [options[@"lineHeight"] doubleValue] : 0.0;
17
+ CGFloat maxWidth = options[@"maxWidth"] ? [options[@"maxWidth"] doubleValue] : 0.0;
18
+ CGFloat maxHeight = options[@"maxHeight"] ? [options[@"maxHeight"] doubleValue] : 0.0;
19
+ NSInteger maxLines = options[@"numberOfLines"] ? [options[@"numberOfLines"] integerValue]: 0;
20
+
21
+ if (maxWidth <= 0) maxWidth = CGFLOAT_MAX;
22
+ if (maxHeight <= 0) maxHeight = CGFLOAT_MAX;
23
+
24
+ // ── 2. Resolve UIFontWeight ────────────────────────────────────────────────
25
+ UIFontWeight uiFontWeight = UIFontWeightRegular;
26
+ NSDictionary *weightMap = @{
27
+ @"100": @(UIFontWeightUltraLight),
28
+ @"200": @(UIFontWeightThin),
29
+ @"300": @(UIFontWeightLight),
30
+ @"400": @(UIFontWeightRegular),
31
+ @"500": @(UIFontWeightMedium),
32
+ @"600": @(UIFontWeightSemibold),
33
+ @"700": @(UIFontWeightBold),
34
+ @"800": @(UIFontWeightHeavy),
35
+ @"900": @(UIFontWeightBlack),
36
+ @"bold": @(UIFontWeightBold),
37
+ @"normal": @(UIFontWeightRegular),
38
+ };
39
+ if (weightMap[fontWeight]) {
40
+ uiFontWeight = [weightMap[fontWeight] doubleValue];
41
+ }
42
+
43
+ BOOL isItalic = [fontStyle isEqualToString:@"italic"];
44
+
45
+ // ── 3. Build UIFont ────────────────────────────────────────────────────────
46
+ UIFont *font = nil;
47
+
48
+ if (fontFamily.length > 0) {
49
+ // Build a descriptor for the requested family so we can apply traits
50
+ UIFontDescriptor *descriptor = [UIFontDescriptor fontDescriptorWithName:fontFamily size:fontSize];
51
+
52
+ UIFontDescriptorSymbolicTraits traits = 0;
53
+ if (uiFontWeight >= UIFontWeightBold) traits |= UIFontDescriptorTraitBold;
54
+ if (isItalic) traits |= UIFontDescriptorTraitItalic;
55
+
56
+ if (traits != 0) {
57
+ UIFontDescriptor *traitDescriptor = [descriptor fontDescriptorWithSymbolicTraits:traits];
58
+ // fontDescriptorWithSymbolicTraits returns nil if the traits can't be satisfied
59
+ if (traitDescriptor) descriptor = traitDescriptor;
60
+ }
61
+
62
+ font = [UIFont fontWithDescriptor:descriptor size:fontSize];
63
+ }
64
+
65
+ // Fallback: system font (always available)
66
+ if (!font) {
67
+ if (isItalic) {
68
+ UIFontDescriptor *sysDesc = [[UIFont systemFontOfSize:fontSize weight:uiFontWeight] fontDescriptor];
69
+ UIFontDescriptor *italicDesc = [sysDesc fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic];
70
+ font = italicDesc ? [UIFont fontWithDescriptor:italicDesc size:fontSize]
71
+ : [UIFont systemFontOfSize:fontSize weight:uiFontWeight];
72
+ } else {
73
+ font = [UIFont systemFontOfSize:fontSize weight:uiFontWeight];
74
+ }
75
+ }
76
+
77
+ // Ultimate fallback
78
+ if (!font) font = [UIFont systemFontOfSize:fontSize];
79
+
80
+ // ── 4. Build paragraph style ───────────────────────────────────────────────
81
+ NSMutableParagraphStyle *paraStyle = [[NSMutableParagraphStyle alloc] init];
82
+ paraStyle.lineBreakMode = NSLineBreakByWordWrapping;
83
+
84
+ if (lineHeight > 0.0) {
85
+ // Setting both min and max to the same value gives an exact line height,
86
+ // which matches React Native's lineHeight prop behaviour.
87
+ paraStyle.minimumLineHeight = lineHeight;
88
+ paraStyle.maximumLineHeight = lineHeight;
89
+ }
90
+
91
+ // ── 5. Build attributed string ─────────────────────────────────────────────
92
+ NSMutableDictionary *attrs = [@{
93
+ NSFontAttributeName: font,
94
+ NSParagraphStyleAttributeName: paraStyle,
95
+ } mutableCopy];
96
+
97
+ if (letterSpacing != 0.0) {
98
+ // NSKernAttributeName is in points — same unit as fontSize on iOS.
99
+ attrs[NSKernAttributeName] = @(letterSpacing);
100
+ }
101
+
102
+ NSString *safeText = (text.length > 0) ? text : @"";
103
+ NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:safeText
104
+ attributes:attrs];
105
+
106
+ // ── 6. TextKit layout ──────────────────────────────────────────────────────
107
+ //
108
+ // We use the full TextKit stack (NSTextStorage → NSLayoutManager → NSTextContainer)
109
+ // rather than boundingRectWithSize:options:context: because:
110
+ // • It correctly handles numberOfLines (via maximumNumberOfLines).
111
+ // • It gives us a real lineCount via enumerateLineFragments.
112
+ // • It is the same engine UILabel uses internally.
113
+ //
114
+ NSTextStorage *storage = [[NSTextStorage alloc] initWithAttributedString:attrString];
115
+ NSLayoutManager *manager = [[NSLayoutManager alloc] init];
116
+ NSTextContainer *container = [[NSTextContainer alloc] initWithSize:CGSizeMake(maxWidth, maxHeight)];
117
+
118
+ container.lineFragmentPadding = 0.0; // no extra horizontal padding
119
+ container.lineBreakMode = NSLineBreakByWordWrapping;
120
+ if (maxLines > 0) container.maximumNumberOfLines = (NSUInteger)maxLines;
121
+
122
+ [manager addTextContainer:container];
123
+ [storage addLayoutManager:manager];
124
+
125
+ // Force complete layout pass
126
+ [manager ensureLayoutForTextContainer:container];
127
+
128
+ CGRect usedRect = [manager usedRectForTextContainer:container];
129
+
130
+ // Count lines by enumerating actual line fragments
131
+ __block NSInteger lineCount = 0;
132
+ NSRange glyphRange = [manager glyphRangeForTextContainer:container];
133
+ [manager enumerateLineFragmentsForGlyphRange:glyphRange
134
+ usingBlock:^(CGRect rect,
135
+ CGRect usedLineRect,
136
+ NSTextContainer *tc,
137
+ NSRange glyphRng,
138
+ BOOL *stop) {
139
+ lineCount++;
140
+ }];
141
+
142
+ // ceil to avoid sub-pixel gaps when the caller uses these values for layout
143
+ CGFloat measuredWidth = ceil(usedRect.size.width);
144
+ CGFloat measuredHeight = ceil(usedRect.size.height);
145
+
146
+ return @{
147
+ @"width": @(measuredWidth),
148
+ @"height": @(measuredHeight),
149
+ @"lineCount": @(lineCount),
150
+ };
151
+ }
152
+
153
+
154
+ // ─────────────────────────────────────────────────────────────────────────────
155
+ // Module
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+
158
+ @implementation ReactNativeTextMeasure
159
+
160
+ RCT_EXPORT_MODULE()
161
+
162
+ // ── Async (recommended) ───────────────────────────────────────────────────────
163
+ /**
164
+ * measureText(text, options) → Promise<{ width, height, lineCount }>
165
+ *
166
+ * Runs on a background serial queue — does NOT block the JS thread.
167
+ */
168
+ RCT_EXPORT_METHOD(measureText:(NSString *)text
169
+ options:(NSDictionary *)options
170
+ resolver:(RCTPromiseResolveBlock)resolve
171
+ rejecter:(RCTPromiseRejectBlock)reject)
172
+ {
173
+ // Capture to avoid potential mutation
174
+ NSString *capturedText = [text copy];
175
+ NSDictionary *capturedOptions = [options copy];
176
+
177
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
178
+ @try {
179
+ NSDictionary *result = RNTMPerformMeasure(capturedText, capturedOptions ?: @{});
180
+ resolve(result);
181
+ } @catch (NSException *exception) {
182
+ reject(@"MEASURE_ERROR", exception.reason, nil);
183
+ }
184
+ });
185
+ }
186
+
187
+ // ── Synchronous (use sparingly) ───────────────────────────────────────────────
188
+ /**
189
+ * measureTextSync(text, options) → { width, height, lineCount }
190
+ *
191
+ * Runs on the JS thread. Safe for use in synchronous layout code but
192
+ * will block JS execution for the duration of the measurement.
193
+ */
194
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(measureTextSync:(NSString *)text
195
+ options:(NSDictionary *)options)
196
+ {
197
+ @try {
198
+ return RNTMPerformMeasure(text ?: @"", options ?: @{});
199
+ } @catch (NSException *exception) {
200
+ return @{ @"error": exception.reason ?: @"Unknown error" };
201
+ }
202
+ }
203
+
204
+ @end
@@ -0,0 +1,281 @@
1
+ // !$*UTF8*$!
2
+ {
3
+ archiveVersion = 1;
4
+ classes = {
5
+ };
6
+ objectVersion = 46;
7
+ objects = {
8
+
9
+ /* Begin PBXCopyFilesBuildPhase section */
10
+ 58B511D91A9E6C8500147676 /* CopyFiles */ = {
11
+ isa = PBXCopyFilesBuildPhase;
12
+ buildActionMask = 2147483647;
13
+ dstPath = "include/$(PRODUCT_NAME)";
14
+ dstSubfolderSpec = 16;
15
+ files = (
16
+ );
17
+ runOnlyForDeploymentPostprocessing = 0;
18
+ };
19
+ /* End PBXCopyFilesBuildPhase section */
20
+
21
+ /* Begin PBXFileReference section */
22
+ 134814201AA4EA6300B7C361 /* libReactNativeTextMeasure.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactNativeTextMeasure.a; sourceTree = BUILT_PRODUCTS_DIR; };
23
+ /* End PBXFileReference section */
24
+
25
+ /* Begin PBXFrameworksBuildPhase section */
26
+ 58B511D81A9E6C8500147676 /* Frameworks */ = {
27
+ isa = PBXFrameworksBuildPhase;
28
+ buildActionMask = 2147483647;
29
+ files = (
30
+ );
31
+ runOnlyForDeploymentPostprocessing = 0;
32
+ };
33
+ /* End PBXFrameworksBuildPhase section */
34
+
35
+ /* Begin PBXGroup section */
36
+ 134814211AA4EA7D00B7C361 /* Products */ = {
37
+ isa = PBXGroup;
38
+ children = (
39
+ 134814201AA4EA6300B7C361 /* libReactNativeTextMeasure.a */,
40
+ );
41
+ name = Products;
42
+ sourceTree = "<group>";
43
+ };
44
+ 58B511D21A9E6C8500147676 = {
45
+ isa = PBXGroup;
46
+ children = (
47
+ 134814211AA4EA7D00B7C361 /* Products */,
48
+ );
49
+ sourceTree = "<group>";
50
+ };
51
+ /* End PBXGroup section */
52
+
53
+ /* Begin PBXNativeTarget section */
54
+ 58B511DA1A9E6C8500147676 /* ReactNativeTextMeasure */ = {
55
+ isa = PBXNativeTarget;
56
+ buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativeTextMeasure" */;
57
+ buildPhases = (
58
+ 58B511D71A9E6C8500147676 /* Sources */,
59
+ 58B511D81A9E6C8500147676 /* Frameworks */,
60
+ 58B511D91A9E6C8500147676 /* CopyFiles */,
61
+ );
62
+ buildRules = (
63
+ );
64
+ dependencies = (
65
+ );
66
+ name = ReactNativeTextMeasure;
67
+ productName = RCTDataManager;
68
+ productReference = 134814201AA4EA6300B7C361 /* libReactNativeTextMeasure.a */;
69
+ productType = "com.apple.product-type.library.static";
70
+ };
71
+ /* End PBXNativeTarget section */
72
+
73
+ /* Begin PBXProject section */
74
+ 58B511D31A9E6C8500147676 /* Project object */ = {
75
+ isa = PBXProject;
76
+ attributes = {
77
+ LastUpgradeCheck = 0920;
78
+ ORGANIZATIONNAME = Facebook;
79
+ TargetAttributes = {
80
+ 58B511DA1A9E6C8500147676 = {
81
+ CreatedOnToolsVersion = 6.1.1;
82
+ };
83
+ };
84
+ };
85
+ buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativeTextMeasure" */;
86
+ compatibilityVersion = "Xcode 3.2";
87
+ developmentRegion = en;
88
+ hasScannedForEncodings = 0;
89
+ knownRegions = (
90
+ en,
91
+ Base,
92
+ );
93
+ mainGroup = 58B511D21A9E6C8500147676;
94
+ productRefGroup = 58B511D21A9E6C8500147676;
95
+ projectDirPath = "";
96
+ projectRoot = "";
97
+ targets = (
98
+ 58B511DA1A9E6C8500147676 /* ReactNativeTextMeasure */,
99
+ );
100
+ };
101
+ /* End PBXProject section */
102
+
103
+ /* Begin PBXSourcesBuildPhase section */
104
+ 58B511D71A9E6C8500147676 /* Sources */ = {
105
+ isa = PBXSourcesBuildPhase;
106
+ buildActionMask = 2147483647;
107
+ files = (
108
+ );
109
+ runOnlyForDeploymentPostprocessing = 0;
110
+ };
111
+ /* End PBXSourcesBuildPhase section */
112
+
113
+ /* Begin XCBuildConfiguration section */
114
+ 58B511ED1A9E6C8500147676 /* Debug */ = {
115
+ isa = XCBuildConfiguration;
116
+ buildSettings = {
117
+ ALWAYS_SEARCH_USER_PATHS = NO;
118
+ CLANG_ANALYZER_NONNULL = YES;
119
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
120
+ CLANG_CXX_LIBRARY = "libc++";
121
+ CLANG_ENABLE_MODULES = YES;
122
+ CLANG_ENABLE_OBJC_ARC = YES;
123
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
124
+ CLANG_WARN_BOOL_CONVERSION = YES;
125
+ CLANG_WARN_COMMA = YES;
126
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
127
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
128
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
129
+ CLANG_WARN_EMPTY_BODY = YES;
130
+ CLANG_WARN_ENUM_CONVERSION = YES;
131
+ CLANG_WARN_INFINITE_RECURSION = YES;
132
+ CLANG_WARN_INT_CONVERSION = YES;
133
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
134
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
135
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
136
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
137
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
138
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
139
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
140
+ CLANG_WARN_UNREACHABLE_CODE = YES;
141
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
142
+ COPY_PHASE_STRIP = NO;
143
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
144
+ ENABLE_TESTABILITY = YES;
145
+ GCC_C_LANGUAGE_STANDARD = gnu99;
146
+ GCC_DYNAMIC_NO_PIC = NO;
147
+ GCC_NO_COMMON_BLOCKS = YES;
148
+ GCC_OPTIMIZATION_LEVEL = 0;
149
+ GCC_PREPROCESSOR_DEFINITIONS = (
150
+ "DEBUG=1",
151
+ "$(inherited)",
152
+ );
153
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
154
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
155
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
156
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
157
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
158
+ GCC_WARN_UNUSED_FUNCTION = YES;
159
+ GCC_WARN_UNUSED_VARIABLE = YES;
160
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
161
+ LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
162
+ LIBRARY_SEARCH_PATHS = (
163
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
164
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
165
+ "\"$(inherited)\"",
166
+ );
167
+ MTL_ENABLE_DEBUG_INFO = YES;
168
+ ONLY_ACTIVE_ARCH = YES;
169
+ SDKROOT = iphoneos;
170
+ };
171
+ name = Debug;
172
+ };
173
+ 58B511EE1A9E6C8500147676 /* Release */ = {
174
+ isa = XCBuildConfiguration;
175
+ buildSettings = {
176
+ ALWAYS_SEARCH_USER_PATHS = NO;
177
+ CLANG_ANALYZER_NONNULL = YES;
178
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
179
+ CLANG_CXX_LIBRARY = "libc++";
180
+ CLANG_ENABLE_MODULES = YES;
181
+ CLANG_ENABLE_OBJC_ARC = YES;
182
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
183
+ CLANG_WARN_BOOL_CONVERSION = YES;
184
+ CLANG_WARN_COMMA = YES;
185
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
186
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
187
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
188
+ CLANG_WARN_EMPTY_BODY = YES;
189
+ CLANG_WARN_ENUM_CONVERSION = YES;
190
+ CLANG_WARN_INFINITE_RECURSION = YES;
191
+ CLANG_WARN_INT_CONVERSION = YES;
192
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
193
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
194
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
195
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
196
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
197
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
198
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
199
+ CLANG_WARN_UNREACHABLE_CODE = YES;
200
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
201
+ COPY_PHASE_STRIP = YES;
202
+ ENABLE_NS_ASSERTIONS = NO;
203
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
204
+ GCC_C_LANGUAGE_STANDARD = gnu99;
205
+ GCC_NO_COMMON_BLOCKS = YES;
206
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
207
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
208
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
209
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
210
+ GCC_WARN_UNUSED_FUNCTION = YES;
211
+ GCC_WARN_UNUSED_VARIABLE = YES;
212
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
213
+ LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
214
+ LIBRARY_SEARCH_PATHS = (
215
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
216
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
217
+ "\"$(inherited)\"",
218
+ );
219
+ MTL_ENABLE_DEBUG_INFO = NO;
220
+ SDKROOT = iphoneos;
221
+ VALIDATE_PRODUCT = YES;
222
+ };
223
+ name = Release;
224
+ };
225
+ 58B511F01A9E6C8500147676 /* Debug */ = {
226
+ isa = XCBuildConfiguration;
227
+ buildSettings = {
228
+ HEADER_SEARCH_PATHS = (
229
+ "$(inherited)",
230
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
231
+ "$(SRCROOT)/../../../React/**",
232
+ "$(SRCROOT)/../../react-native/React/**",
233
+ );
234
+ LIBRARY_SEARCH_PATHS = "$(inherited)";
235
+ OTHER_LDFLAGS = "-ObjC";
236
+ PRODUCT_NAME = ReactNativeTextMeasure;
237
+ SKIP_INSTALL = YES;
238
+ };
239
+ name = Debug;
240
+ };
241
+ 58B511F11A9E6C8500147676 /* Release */ = {
242
+ isa = XCBuildConfiguration;
243
+ buildSettings = {
244
+ HEADER_SEARCH_PATHS = (
245
+ "$(inherited)",
246
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
247
+ "$(SRCROOT)/../../../React/**",
248
+ "$(SRCROOT)/../../react-native/React/**",
249
+ );
250
+ LIBRARY_SEARCH_PATHS = "$(inherited)";
251
+ OTHER_LDFLAGS = "-ObjC";
252
+ PRODUCT_NAME = ReactNativeTextMeasure;
253
+ SKIP_INSTALL = YES;
254
+ };
255
+ name = Release;
256
+ };
257
+ /* End XCBuildConfiguration section */
258
+
259
+ /* Begin XCConfigurationList section */
260
+ 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativeTextMeasure" */ = {
261
+ isa = XCConfigurationList;
262
+ buildConfigurations = (
263
+ 58B511ED1A9E6C8500147676 /* Debug */,
264
+ 58B511EE1A9E6C8500147676 /* Release */,
265
+ );
266
+ defaultConfigurationIsVisible = 0;
267
+ defaultConfigurationName = Release;
268
+ };
269
+ 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativeTextMeasure" */ = {
270
+ isa = XCConfigurationList;
271
+ buildConfigurations = (
272
+ 58B511F01A9E6C8500147676 /* Debug */,
273
+ 58B511F11A9E6C8500147676 /* Release */,
274
+ );
275
+ defaultConfigurationIsVisible = 0;
276
+ defaultConfigurationName = Release;
277
+ };
278
+ /* End XCConfigurationList section */
279
+ };
280
+ rootObject = 58B511D31A9E6C8500147676 /* Project object */;
281
+ }
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Workspace
3
+ version = "1.0">
4
+ <FileRef
5
+ location = "group:ReactNativeTextMeasure.xcodeproj">
6
+ </FileRef>
7
+ </Workspace>
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "react-native-text-measure",
3
+ "version": "1.0.0",
4
+ "description": "Accurately measure text dimensions (width, height, lineCount) using native text layout engines — no view rendered.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/AminAllahham/react-native-text-measure.git"
10
+ },
11
+ "keywords": [
12
+ "react-native",
13
+ "text",
14
+ "measure",
15
+ "height",
16
+ "width",
17
+ "layout",
18
+ "native-module"
19
+ ],
20
+ "author": "Amin Allahham <amin.allahham9@gmail.com>",
21
+ "license": "MIT",
22
+ "peerDependencies": {
23
+ "react-native": ">=0.60.0"
24
+ },
25
+ "devDependencies": {
26
+ "react-native": "^0.73.0"
27
+ }
28
+ }
@@ -0,0 +1,30 @@
1
+ # react-native-text-measure.podspec
2
+
3
+ require "json"
4
+
5
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
6
+
7
+ Pod::Spec.new do |s|
8
+ s.name = "react-native-text-measure"
9
+ s.version = package["version"]
10
+ s.summary = package["description"]
11
+ s.description = <<-DESC
12
+ react-native-text-measure
13
+ DESC
14
+ s.homepage = "https://github.com/github_account/react-native-text-measure"
15
+ # brief license entry:
16
+ s.license = "MIT"
17
+ # optional - use expanded license entry instead:
18
+ # s.license = { :type => "MIT", :file => "LICENSE" }
19
+ s.authors = { "Your Name" => "yourname@email.com" }
20
+ s.platforms = { :ios => "9.0" }
21
+ s.source = { :git => "https://github.com/github_account/react-native-text-measure.git", :tag => "#{s.version}" }
22
+
23
+ s.source_files = "ios/**/*.{h,c,cc,cpp,m,mm,swift}"
24
+ s.requires_arc = true
25
+
26
+ s.dependency "React"
27
+ # ...
28
+ # s.dependency "..."
29
+ end
30
+