├── .watchmanconfig ├── example ├── .watchmanconfig ├── jest.config.js ├── .bundle │ └── config ├── .eslintrc.js ├── app.json ├── .prettierrc.js ├── android │ ├── app │ │ ├── debug.keystore │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── values │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ └── drawable │ │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── AndroidManifest.xml │ │ │ │ └── java │ │ │ │ │ └── com │ │ │ │ │ └── nitrotextexample │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── MainApplication.kt │ │ │ └── debug │ │ │ │ └── AndroidManifest.xml │ │ ├── proguard-rules.pro │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ ├── gradle.properties │ └── gradlew.bat ├── ios │ ├── NitroTextExample │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── PrivacyInfo.xcprivacy │ │ ├── AppDelegate.swift │ │ ├── Info.plist │ │ └── LaunchScreen.storyboard │ ├── NitroTextExample.xcworkspace │ │ └── contents.xcworkspacedata │ ├── .xcode.env │ ├── Podfile │ └── NitroTextExample.xcodeproj │ │ └── xcshareddata │ │ └── xcschemes │ │ └── NitroTextExample.xcscheme ├── index.js ├── tsconfig.json ├── react-native.config.js ├── metro.config.js ├── babel.config.js ├── Gemfile ├── .gitignore ├── package.json ├── src │ └── App.tsx ├── Gemfile.lock └── README.md ├── bunfig.toml ├── nitrogen └── generated │ ├── .gitattributes │ ├── ios │ ├── c++ │ │ └── HybridNitroTextSpecSwift.cpp │ ├── NitroTextAutolinking.mm │ ├── swift │ │ ├── Renderer.swift │ │ ├── FontStyle.swift │ │ ├── EllipsizeMode.swift │ │ ├── TextTransform.swift │ │ ├── TextDecorationStyle.swift │ │ ├── TextAlign.swift │ │ ├── LineBreakStrategyIOS.swift │ │ ├── TextDecorationLine.swift │ │ ├── TextLayoutEvent.swift │ │ ├── Func_void.swift │ │ ├── MenuItem.swift │ │ ├── Func_void_TextLayoutEvent.swift │ │ ├── DynamicTypeRamp.swift │ │ ├── FontWeight.swift │ │ ├── TextLayout.swift │ │ └── HybridNitroTextSpec.swift │ ├── NitroTextAutolinking.swift │ ├── NitroText+autolinking.rb │ ├── NitroText-Swift-Cxx-Bridge.cpp │ └── NitroText-Swift-Cxx-Umbrella.hpp │ └── shared │ ├── json │ └── NitroTextConfig.json │ └── c++ │ ├── TextLayoutEvent.hpp │ ├── MenuItem.hpp │ ├── Renderer.hpp │ ├── FontStyle.hpp │ ├── EllipsizeMode.hpp │ ├── TextTransform.hpp │ ├── TextDecorationStyle.hpp │ ├── LineBreakStrategyIOS.hpp │ ├── TextAlign.hpp │ ├── TextDecorationLine.hpp │ └── TextLayout.hpp ├── src ├── index.ts ├── constants │ ├── index.ts │ └── css.ts ├── renderers.ts ├── renderers │ ├── index.ts │ ├── types.ts │ └── utils.ts ├── types.ts ├── specs │ └── nitro-text.nitro.ts └── nitro-text.tsx ├── babel.config.js ├── ios ├── Bridge.h ├── HybridNitroTextComponentOverride.mm ├── NitroTextImpl+Paragraph.swift ├── NitroTextImpl+Fragment.swift └── NitroTextImpl+Attributes.swift ├── react-native.config.js ├── cpp ├── NitroTextUtil.cpp ├── NitroTextUtil.hpp ├── NitroTextComponentDescriptor.hpp ├── NitroTextShadowNode.hpp ├── NitroTextComponentDescriptor.cpp └── NitroTextLogger.hpp ├── jest.config.js ├── nitro.json ├── .github ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── question.yml │ ├── feature_request.yml │ └── bug_report.yml ├── dependabot.yml └── workflows │ ├── release.yml │ ├── android-build.yml │ └── ios-build.yml ├── NitroText.podspec ├── tsconfig.json ├── .gitignore ├── LICENSE ├── release.config.cjs ├── eslint.config.mjs ├── package.json └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [install] 2 | linker = "hoisted" 3 | -------------------------------------------------------------------------------- /nitrogen/generated/.gitattributes: -------------------------------------------------------------------------------- 1 | ** linguist-generated=true 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nitro-text' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './html' 2 | export * from './css' 3 | -------------------------------------------------------------------------------- /example/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | }; 4 | -------------------------------------------------------------------------------- /example/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | } 4 | -------------------------------------------------------------------------------- /example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native', 4 | }; 5 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NitroTextExample", 3 | "displayName": "NitroTextExample" 4 | } 5 | -------------------------------------------------------------------------------- /example/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | }; 6 | -------------------------------------------------------------------------------- /ios/Bridge.h: -------------------------------------------------------------------------------- 1 | // 2 | // Bridge.h 3 | // nitro-text 4 | // 5 | // Created by Patrick Kabwe on 9/1/2025 6 | // 7 | 8 | #pragma once 9 | -------------------------------------------------------------------------------- /src/renderers.ts: -------------------------------------------------------------------------------- 1 | export { renderStringChildren } from './renderers/index' 2 | export type { RenderResult } from './renderers/index' 3 | -------------------------------------------------------------------------------- /example/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/debug.keystore -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | NitroTextExample 3 | 4 | -------------------------------------------------------------------------------- /example/ios/NitroTextExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickkabwe/react-native-nitro-text/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@react-native-community/cli-types').DependencyConfig} 3 | */ 4 | module.exports = { 5 | dependency: { 6 | autolinkTransitiveDependencies: true, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import { AppRegistry } from 'react-native'; 6 | import App from './src/App'; 7 | import { name as appName } from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /cpp/NitroTextUtil.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // NitroTextUtil.cpp 3 | // Utility functions for NitroText 4 | // 5 | 6 | #include "NitroTextUtil.hpp" 7 | 8 | // Currently only contains macros, so this file is mostly empty. 9 | // Kept for future utility functions if needed. 10 | 11 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-native/typescript-config", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["**/node_modules", "**/Pods"], 5 | "compilerOptions": { 6 | "strict": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "react-native-nitro-text": ["../src"] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /example/ios/NitroTextExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | testMatch: ['**/__tests__/**/*.ts', '**/__tests__/**/*.tsx', '**/*.test.ts', '**/*.test.tsx'], 4 | transformIgnorePatterns: [ 5 | 'node_modules/(?!(react-native|@react-native|react-native-nitro-modules|@react-native-community|@react-native/js-polyfills)/)', 6 | ], 7 | } 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { includeBuild("../../node_modules/@react-native/gradle-plugin") } 2 | plugins { id("com.facebook.react.settings") } 3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } 4 | rootProject.name = 'NitroTextExample' 5 | include ':app' 6 | includeBuild('../../node_modules/@react-native/gradle-plugin') 7 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/c++/HybridNitroTextSpecSwift.cpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// HybridNitroTextSpecSwift.cpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #include "HybridNitroTextSpecSwift.hpp" 9 | 10 | namespace margelo::nitro::nitrotext { 11 | } // namespace margelo::nitro::nitrotext 12 | -------------------------------------------------------------------------------- /example/react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const pkg = require('../package.json') 3 | 4 | /** 5 | * @type {import('@react-native-community/cli-types').Config} 6 | */ 7 | module.exports = { 8 | project: { 9 | ios: { 10 | automaticPodsInstallation: true, 11 | }, 12 | }, 13 | dependencies: { 14 | [pkg.name]: { 15 | root: path.join(__dirname, '..'), 16 | }, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); 2 | const path = require('path'); 3 | const root = path.resolve(__dirname, '..'); 4 | 5 | /** 6 | * Metro configuration 7 | * https://facebook.github.io/metro/docs/configuration 8 | * 9 | * @type {import('metro-config').MetroConfig} 10 | */ 11 | const config = { 12 | watchFolders: [root], 13 | }; 14 | 15 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); -------------------------------------------------------------------------------- /nitro.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://nitro.margelo.com/nitro.schema.json", 3 | "cxxNamespace": [ 4 | "nitrotext" 5 | ], 6 | "ios": { 7 | "iosModuleName": "NitroText" 8 | }, 9 | "android": { 10 | "androidNamespace": [ 11 | "nitrotext" 12 | ], 13 | "androidCxxLibName": "NitroText" 14 | }, 15 | "autolinking": { 16 | "NitroText": { 17 | "swift": "HybridNitroText" 18 | } 19 | }, 20 | "ignorePaths": [ 21 | "**/node_modules" 22 | ] 23 | } -------------------------------------------------------------------------------- /src/constants/css.ts: -------------------------------------------------------------------------------- 1 | import type { Fragment } from '../types' 2 | 3 | export const FRAGMENT_STYLE_KEYS: (keyof Fragment)[] = [ 4 | 'selectionColor', 5 | 'fontSize', 6 | 'fontWeight', 7 | 'fontColor', 8 | 'fragmentBackgroundColor', 9 | 'fontStyle', 10 | 'fontFamily', 11 | 'lineHeight', 12 | 'letterSpacing', 13 | 'textAlign', 14 | 'textTransform', 15 | 'textDecorationLine', 16 | 'textDecorationColor', 17 | 'textDecorationStyle', 18 | 'linkUrl', 19 | ] 20 | -------------------------------------------------------------------------------- /example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = api => { 5 | api.cache(true); 6 | return { 7 | presets: ['module:@react-native/babel-preset'], 8 | plugins: [ 9 | [ 10 | 'module-resolver', 11 | { 12 | extensions: ['.js', '.ts', '.json', '.jsx', '.tsx'], 13 | alias: { 14 | [pak.name]: path.join(__dirname, '../', pak.source), 15 | }, 16 | }, 17 | ], 18 | ], 19 | }; 20 | }; -------------------------------------------------------------------------------- /example/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' 9 | gem 'xcodeproj', '< 1.28.0' 10 | gem 'concurrent-ruby', '< 1.3.6' 11 | 12 | # Ruby 3.4.0 has removed some libraries from the standard library. 13 | gem 'bigdecimal' 14 | gem 'logger' 15 | gem 'benchmark' 16 | gem 'mutex_m' 17 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | buildToolsVersion = "36.0.0" 4 | minSdkVersion = 24 5 | compileSdkVersion = 36 6 | targetSdkVersion = 36 7 | ndkVersion = "27.1.12297006" 8 | kotlinVersion = "2.1.20" 9 | } 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle") 16 | classpath("com.facebook.react:react-native-gradle-plugin") 17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 18 | } 19 | } 20 | 21 | apply plugin: "com.facebook.react.rootproject" 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | 3 | ## Description 4 | 5 | Brief description of what this PR does. 6 | 7 | ## Type of Change 8 | 9 | - [ ] Bug fix 10 | - [ ] New feature 11 | - [ ] Breaking change 12 | - [ ] Documentation update 13 | - [ ] Other 14 | 15 | ## Testing 16 | 17 | - [ ] Tested on iOS 18 | - [ ] Tested on Android 19 | - [ ] Added/updated tests 20 | 21 | ## Checklist 22 | 23 | - [ ] Code follows project style guidelines 24 | - [ ] Self-review completed 25 | - [ ] Documentation updated (if needed) 26 | - [ ] No breaking changes (or clearly documented) 27 | 28 | ## Additional Notes 29 | 30 | Any additional information for reviewers. 31 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/NitroTextAutolinking.mm: -------------------------------------------------------------------------------- 1 | /// 2 | /// NitroTextAutolinking.mm 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #import 9 | #import 10 | #import "NitroText-Swift-Cxx-Umbrella.hpp" 11 | #import 12 | 13 | #include "HybridNitroTextSpecSwift.hpp" 14 | 15 | @interface NitroTextAutolinking : NSObject 16 | @end 17 | 18 | @implementation NitroTextAutolinking 19 | 20 | + (void) load { 21 | using namespace margelo::nitro; 22 | using namespace margelo::nitro::nitrotext; 23 | 24 | HybridObjectRegistry::registerHybridObjectConstructor( 25 | "NitroText", 26 | []() -> std::shared_ptr { 27 | std::shared_ptr hybridObject = NitroText::NitroTextAutolinking::createNitroText(); 28 | return hybridObject; 29 | } 30 | ); 31 | } 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /NitroText.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = "NitroText" 7 | s.version = package["version"] 8 | s.summary = package["description"] 9 | s.homepage = package["homepage"] 10 | s.license = package["license"] 11 | s.authors = package["author"] 12 | 13 | s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0 } 14 | s.source = { :git => "https://github.com/patrickkabwe/react-native-nitro-text.git", :tag => "#{s.version}" } 15 | 16 | s.source_files = [ 17 | # Implementation (Swift) 18 | "ios/**/*.{swift}", 19 | # Autolinking/Registration (Objective-C++) 20 | "ios/**/*.{m,mm,hpp}", 21 | # Implementation (C++ objects) 22 | "cpp/**/*.{hpp,cpp}", 23 | ] 24 | 25 | load 'nitrogen/generated/ios/NitroText+autolinking.rb' 26 | add_nitrogen_files(s) 27 | 28 | s.dependency 'React-jsi' 29 | s.dependency 'React-callinvoker' 30 | install_modules_dependencies(s) 31 | end 32 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/Renderer.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// Renderer.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /** 9 | * Represents the JS union `Renderer`, backed by a C++ enum. 10 | */ 11 | public typealias Renderer = margelo.nitro.nitrotext.Renderer 12 | 13 | public extension Renderer { 14 | /** 15 | * Get a Renderer for the given String value, or 16 | * return `nil` if the given value was invalid/unknown. 17 | */ 18 | init?(fromString string: String) { 19 | switch string { 20 | case "html": 21 | self = .html 22 | case "plaintext": 23 | self = .plaintext 24 | default: 25 | return nil 26 | } 27 | } 28 | 29 | /** 30 | * Get the String value this Renderer represents. 31 | */ 32 | var stringValue: String { 33 | switch self { 34 | case .html: 35 | return "html" 36 | case .plaintext: 37 | return "plaintext" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cpp/NitroTextUtil.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // NitroTextUtil.hpp 3 | // Utility macros for React Native version checking 4 | // 5 | 6 | #pragma once 7 | 8 | #if __has_include() 9 | #include 10 | #endif 11 | 12 | /** 13 | * @spec RN_VERSION_AT_LEAST 14 | * @brief Checks if React Native version is >= (major.minor) 15 | * @param major Major version number (e.g., 0, 1, 2) 16 | * @param minor Minor version number (e.g., 81, 82, 83) 17 | * @return Non-zero if version >= (major.minor), 0 otherwise 18 | * 19 | * @example 20 | * ```cpp 21 | * #if RN_VERSION_AT_LEAST(0, 81) 22 | * // Code for RN >= 0.81 23 | * #endif 24 | * ``` 25 | */ 26 | #if defined(REACT_NATIVE_VERSION_MAJOR) 27 | #define RN_VERSION_AT_LEAST(major, minor) \ 28 | ((REACT_NATIVE_VERSION_MAJOR > (major)) || \ 29 | (REACT_NATIVE_VERSION_MAJOR == (major) && REACT_NATIVE_VERSION_MINOR >= (minor))) 30 | #else 31 | #define RN_VERSION_AT_LEAST(major, minor) 0 32 | #endif 33 | 34 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/NitroTextAutolinking.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// NitroTextAutolinking.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | public final class NitroTextAutolinking { 9 | public typealias bridge = margelo.nitro.nitrotext.bridge.swift 10 | 11 | /** 12 | * Creates an instance of a Swift class that implements `HybridNitroTextSpec`, 13 | * and wraps it in a Swift class that can directly interop with C++ (`HybridNitroTextSpec_cxx`) 14 | * 15 | * This is generated by Nitrogen and will initialize the class specified 16 | * in the `"autolinking"` property of `nitro.json` (in this case, `HybridNitroText`). 17 | */ 18 | public static func createNitroText() -> bridge.std__shared_ptr_HybridNitroTextSpec_ { 19 | let hybridObject = HybridNitroText() 20 | return { () -> bridge.std__shared_ptr_HybridNitroTextSpec_ in 21 | let __cxxWrapped = hybridObject.getCxxWrapper() 22 | return __cxxWrapped.getCxxPart() 23 | }() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "allowUnreachableCode": false, 5 | "allowUnusedLabels": false, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "jsx": "react", 9 | "lib": ["esnext"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noEmit": false, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "noImplicitUseStrict": false, 16 | "noStrictGenericChecks": false, 17 | "noUncheckedIndexedAccess": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "resolveJsonModule": true, 21 | "skipLibCheck": true, 22 | "strict": true, 23 | "target": "esnext", 24 | "verbatimModuleSyntax": true 25 | }, 26 | "exclude": [ 27 | "**/node_modules", 28 | "**/lib", 29 | "**/.eslintrc.js", 30 | "**/.prettierrc.js", 31 | "**/jest.config.js", 32 | "**/babel.config.js", 33 | "**/metro.config.js", 34 | "**/tsconfig.json" 35 | ], 36 | "include": ["src/**/*", "nitrogen/**/*.json"] 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Yarn 64 | .yarn/* 65 | !.yarn/patches 66 | !.yarn/plugins 67 | !.yarn/releases 68 | !.yarn/sdks 69 | !.yarn/versions 70 | .kotlin 71 | 72 | # Expo 73 | .expo/ 74 | 75 | # generated by bob 76 | lib/ 77 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /src/renderers/index.ts: -------------------------------------------------------------------------------- 1 | import type { Fragment, Renderer } from '../types' 2 | import type { RenderResult } from './types' 3 | import { HTMLRenderer } from './html' 4 | import { 5 | appendText, 6 | createState, 7 | finalizeState, 8 | trimTrailingNewlines, 9 | } from './utils' 10 | 11 | export function renderStringChildren( 12 | input: string, 13 | renderer: Renderer, 14 | baseFragment: Partial = {} 15 | ): RenderResult { 16 | const text = input ?? '' 17 | switch (renderer) { 18 | case 'html': 19 | return HTMLRenderer.render(text, baseFragment) 20 | case 'plaintext': 21 | default: 22 | return renderPlain(text, baseFragment) 23 | } 24 | } 25 | 26 | function renderPlain(text: string, base: Partial): RenderResult { 27 | const state = createState() 28 | appendText(state, text, base) 29 | trimTrailingNewlines(state) 30 | return finalizeState(state) 31 | } 32 | 33 | export { HTMLRenderer } from './html' 34 | export { CSSProcessor } from './css-processor' 35 | export type { RenderResult, Stylesheet, AppliedStyle } from './types' 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: ❓ Question 2 | description: Ask a question or get help 3 | title: '[Question]: ' 4 | labels: ['question'] 5 | assignees: [] 6 | 7 | body: 8 | - type: textarea 9 | id: question 10 | attributes: 11 | label: What's your question? 12 | description: What would you like to know? 13 | placeholder: Your question here... 14 | validations: 15 | required: true 16 | 17 | - type: textarea 18 | id: tried 19 | attributes: 20 | label: What have you tried? 21 | description: What approaches have you already tried? 22 | placeholder: Describe what you've tried so far... 23 | 24 | - type: textarea 25 | id: code 26 | attributes: 27 | label: Code (if applicable) 28 | description: Relevant code sample 29 | placeholder: | 30 | ```tsx 31 | // Your code here 32 | ``` 33 | render: tsx 34 | 35 | - type: checkboxes 36 | id: checklist 37 | attributes: 38 | label: Checklist 39 | options: 40 | - label: I have searched existing issues and discussions 41 | required: true 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Patrick Kabwe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/ios/NitroTextExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/ios/NitroTextExample/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryUserDefaults 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | CA92.1 21 | 22 | 23 | 24 | NSPrivacyAccessedAPIType 25 | NSPrivacyAccessedAPICategorySystemBootTime 26 | NSPrivacyAccessedAPITypeReasons 27 | 28 | 35F9.1 29 | 30 | 31 | 32 | NSPrivacyCollectedDataTypes 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/FontStyle.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// FontStyle.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /** 9 | * Represents the JS union `FontStyle`, backed by a C++ enum. 10 | */ 11 | public typealias FontStyle = margelo.nitro.nitrotext.FontStyle 12 | 13 | public extension FontStyle { 14 | /** 15 | * Get a FontStyle for the given String value, or 16 | * return `nil` if the given value was invalid/unknown. 17 | */ 18 | init?(fromString string: String) { 19 | switch string { 20 | case "normal": 21 | self = .normal 22 | case "italic": 23 | self = .italic 24 | case "oblique": 25 | self = .oblique 26 | default: 27 | return nil 28 | } 29 | } 30 | 31 | /** 32 | * Get the String value this FontStyle represents. 33 | */ 34 | var stringValue: String { 35 | switch self { 36 | case .normal: 37 | return "normal" 38 | case .italic: 39 | return "italic" 40 | case .oblique: 41 | return "oblique" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | platform :ios, min_ios_version_supported 9 | prepare_react_native_project! 10 | 11 | linkage = ENV['USE_FRAMEWORKS'] 12 | if linkage != nil 13 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 14 | use_frameworks! :linkage => linkage.to_sym 15 | end 16 | 17 | target 'NitroTextExample' do 18 | config = use_native_modules! 19 | 20 | use_react_native!( 21 | :path => config[:reactNativePath], 22 | # An absolute path to your application root. 23 | :app_path => "#{Pod::Config.instance.installation_root}/.." 24 | ) 25 | 26 | post_install do |installer| 27 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 28 | react_native_post_install( 29 | installer, 30 | config[:reactNativePath], 31 | :mac_catalyst_enabled => false, 32 | # :ccache_enabled => true 33 | ) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/json/NitroTextConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "uiViewClassName": "NitroText", 3 | "supportsRawText": false, 4 | "bubblingEventTypes": {}, 5 | "directEventTypes": {}, 6 | "validAttributes": { 7 | "fragments": true, 8 | "renderer": true, 9 | "selectable": true, 10 | "allowFontScaling": true, 11 | "ellipsizeMode": true, 12 | "numberOfLines": true, 13 | "lineBreakStrategyIOS": true, 14 | "dynamicTypeRamp": true, 15 | "maxFontSizeMultiplier": true, 16 | "adjustsFontSizeToFit": true, 17 | "minimumFontScale": true, 18 | "menus": true, 19 | "onTextLayout": true, 20 | "onPress": true, 21 | "onPressIn": true, 22 | "onPressOut": true, 23 | "text": true, 24 | "selectionColor": true, 25 | "fontSize": true, 26 | "fontWeight": true, 27 | "fontColor": true, 28 | "fragmentBackgroundColor": true, 29 | "fontStyle": true, 30 | "fontFamily": true, 31 | "lineHeight": true, 32 | "letterSpacing": true, 33 | "textAlign": true, 34 | "textTransform": true, 35 | "textDecorationLine": true, 36 | "textDecorationColor": true, 37 | "textDecorationStyle": true, 38 | "hybridRef": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cpp/NitroTextComponentDescriptor.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // NitroTextComponentDescriptor.hpp 3 | // Custom, non-generated ComponentDescriptor for NitroText 4 | // 5 | 6 | #pragma once 7 | 8 | #include "../cpp/NitroTextShadowNode.hpp" 9 | #include 10 | 11 | namespace margelo::nitro::nitrotext::views { 12 | 13 | /** 14 | * The Component Descriptor for the "NitroText" View. 15 | */ 16 | class NitroTextComponentDescriptor final: public react::ConcreteComponentDescriptor { 17 | public: 18 | NitroTextComponentDescriptor(const react::ComponentDescriptorParameters& parameters); 19 | 20 | public: 21 | /** 22 | * A faster path for cloning props - reuses the caching logic from `HybridNitroTextProps`. 23 | */ 24 | std::shared_ptr cloneProps(const react::PropsParserContext& context, 25 | const std::shared_ptr& props, 26 | react::RawProps rawProps) const override; 27 | 28 | void adopt(react::ShadowNode& shadowNode) const override; 29 | }; 30 | 31 | } // namespace margelo::nitro::nitrotext::views 32 | -------------------------------------------------------------------------------- /src/renderers/types.ts: -------------------------------------------------------------------------------- 1 | import type { Fragment } from '../types' 2 | 3 | export type RenderResult = { 4 | fragments: Fragment[] 5 | text: string 6 | } 7 | 8 | export type AppendState = { 9 | fragments: Fragment[] 10 | plainText: string 11 | } 12 | 13 | export type ElementNode = { 14 | type: 'element' 15 | tag: string 16 | attrs: Record 17 | children: Node[] 18 | } 19 | 20 | export type TextNode = { 21 | type: 'text' 22 | content: string 23 | } 24 | 25 | export type Node = ElementNode | TextNode 26 | 27 | export type Stylesheet = { 28 | tag: Map>> 29 | className: Map>> 30 | id: Map>> 31 | } 32 | 33 | export type AppliedStyle = { 34 | fragment?: Partial 35 | hidden: boolean 36 | suppressNewlines: boolean 37 | } 38 | 39 | export type WalkContext = { 40 | style: Partial 41 | preformatted: boolean 42 | } 43 | 44 | export type ListStackItem = { 45 | type: 'ul' | 'ol' 46 | index: number 47 | } 48 | 49 | export type WalkState = { 50 | stylesheet: Stylesheet 51 | state: AppendState 52 | listStack: ListStackItem[] 53 | } 54 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: 'github-actions' 5 | directory: '/' 6 | schedule: 7 | interval: 'daily' 8 | labels: 9 | - 'dependencies' 10 | 11 | - package-ecosystem: 'gradle' 12 | directories: 13 | - '/android/' 14 | - '/example/android/' 15 | schedule: 16 | interval: 'daily' 17 | labels: 18 | - 'nitro-core' 19 | - 'nitrogen' 20 | - 'dependencies' 21 | - 'kotlin' 22 | 23 | - package-ecosystem: 'bundler' 24 | directory: '/example/' 25 | schedule: 26 | interval: 'daily' 27 | labels: 28 | - 'dependencies' 29 | - 'ruby' 30 | 31 | - package-ecosystem: 'npm' 32 | directories: 33 | - '/example/' 34 | - '/' 35 | schedule: 36 | interval: 'daily' 37 | labels: 38 | - 'nitro-core' 39 | - 'dependencies' 40 | - 'typescript' 41 | - 'nitrogen' 42 | 43 | groups: 44 | react-native-cli: 45 | patterns: 46 | - '@react-native-community/cli*' 47 | babel: 48 | patterns: 49 | - '@babel/*' 50 | react-native: 51 | patterns: 52 | - '@react-native/*' 53 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature Request 2 | description: Suggest a new feature or enhancement 3 | title: '[Feature]: ' 4 | labels: ['enhancement'] 5 | assignees: [] 6 | 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: What would you like? 12 | description: A clear description of the feature 13 | placeholder: Describe the feature... 14 | validations: 15 | required: true 16 | 17 | - type: textarea 18 | id: motivation 19 | attributes: 20 | label: Why is this needed? 21 | description: What problem does this solve? 22 | placeholder: Explain why this would be useful... 23 | 24 | - type: textarea 25 | id: code 26 | attributes: 27 | label: Code example 28 | description: How would you like to use this feature? 29 | placeholder: | 30 | ```tsx 31 | Example 32 | ``` 33 | render: tsx 34 | 35 | - type: checkboxes 36 | id: checklist 37 | attributes: 38 | label: Checklist 39 | options: 40 | - label: I have searched existing issues 41 | required: true 42 | - label: I have provided a clear use case 43 | required: true 44 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/EllipsizeMode.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// EllipsizeMode.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /** 9 | * Represents the JS union `EllipsizeMode`, backed by a C++ enum. 10 | */ 11 | public typealias EllipsizeMode = margelo.nitro.nitrotext.EllipsizeMode 12 | 13 | public extension EllipsizeMode { 14 | /** 15 | * Get a EllipsizeMode for the given String value, or 16 | * return `nil` if the given value was invalid/unknown. 17 | */ 18 | init?(fromString string: String) { 19 | switch string { 20 | case "head": 21 | self = .head 22 | case "middle": 23 | self = .middle 24 | case "tail": 25 | self = .tail 26 | case "clip": 27 | self = .clip 28 | default: 29 | return nil 30 | } 31 | } 32 | 33 | /** 34 | * Get the String value this EllipsizeMode represents. 35 | */ 36 | var stringValue: String { 37 | switch self { 38 | case .head: 39 | return "head" 40 | case .middle: 41 | return "middle" 42 | case .tail: 43 | return "tail" 44 | case .clip: 45 | return "clip" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/nitrotextexample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nitrotextexample 2 | 3 | import android.os.Bundle 4 | import com.facebook.react.ReactActivity 5 | import com.facebook.react.ReactActivityDelegate 6 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 7 | import com.facebook.react.defaults.DefaultReactActivityDelegate 8 | import com.swmansion.rnscreens.fragment.restoration.RNScreensFragmentFactory 9 | 10 | class MainActivity : ReactActivity() { 11 | 12 | /** 13 | * Returns the name of the main component registered from JavaScript. This is used to schedule 14 | * rendering of the component. 15 | */ 16 | override fun getMainComponentName(): String = "NitroTextExample" 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | supportFragmentManager.fragmentFactory = RNScreensFragmentFactory() 20 | super.onCreate(savedInstanceState) 21 | } 22 | 23 | /** 24 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 25 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 26 | */ 27 | override fun createReactActivityDelegate(): ReactActivityDelegate = 28 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) 29 | } 30 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/TextTransform.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextTransform.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /** 9 | * Represents the JS union `TextTransform`, backed by a C++ enum. 10 | */ 11 | public typealias TextTransform = margelo.nitro.nitrotext.TextTransform 12 | 13 | public extension TextTransform { 14 | /** 15 | * Get a TextTransform for the given String value, or 16 | * return `nil` if the given value was invalid/unknown. 17 | */ 18 | init?(fromString string: String) { 19 | switch string { 20 | case "none": 21 | self = .none 22 | case "uppercase": 23 | self = .uppercase 24 | case "lowercase": 25 | self = .lowercase 26 | case "capitalize": 27 | self = .capitalize 28 | default: 29 | return nil 30 | } 31 | } 32 | 33 | /** 34 | * Get the String value this TextTransform represents. 35 | */ 36 | var stringValue: String { 37 | switch self { 38 | case .none: 39 | return "none" 40 | case .uppercase: 41 | return "uppercase" 42 | case .lowercase: 43 | return "lowercase" 44 | case .capitalize: 45 | return "capitalize" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/TextDecorationStyle.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextDecorationStyle.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /** 9 | * Represents the JS union `TextDecorationStyle`, backed by a C++ enum. 10 | */ 11 | public typealias TextDecorationStyle = margelo.nitro.nitrotext.TextDecorationStyle 12 | 13 | public extension TextDecorationStyle { 14 | /** 15 | * Get a TextDecorationStyle for the given String value, or 16 | * return `nil` if the given value was invalid/unknown. 17 | */ 18 | init?(fromString string: String) { 19 | switch string { 20 | case "solid": 21 | self = .solid 22 | case "double": 23 | self = .double 24 | case "dotted": 25 | self = .dotted 26 | case "dashed": 27 | self = .dashed 28 | default: 29 | return nil 30 | } 31 | } 32 | 33 | /** 34 | * Get the String value this TextDecorationStyle represents. 35 | */ 36 | var stringValue: String { 37 | switch self { 38 | case .solid: 39 | return "solid" 40 | case .double: 41 | return "double" 42 | case .dotted: 43 | return "dotted" 44 | case .dashed: 45 | return "dashed" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/TextAlign.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextAlign.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /** 9 | * Represents the JS union `TextAlign`, backed by a C++ enum. 10 | */ 11 | public typealias TextAlign = margelo.nitro.nitrotext.TextAlign 12 | 13 | public extension TextAlign { 14 | /** 15 | * Get a TextAlign for the given String value, or 16 | * return `nil` if the given value was invalid/unknown. 17 | */ 18 | init?(fromString string: String) { 19 | switch string { 20 | case "auto": 21 | self = .auto 22 | case "left": 23 | self = .left 24 | case "right": 25 | self = .right 26 | case "center": 27 | self = .center 28 | case "justify": 29 | self = .justify 30 | default: 31 | return nil 32 | } 33 | } 34 | 35 | /** 36 | * Get the String value this TextAlign represents. 37 | */ 38 | var stringValue: String { 39 | switch self { 40 | case .auto: 41 | return "auto" 42 | case .left: 43 | return "left" 44 | case .right: 45 | return "right" 46 | case .center: 47 | return "center" 48 | case .justify: 49 | return "justify" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/LineBreakStrategyIOS.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// LineBreakStrategyIOS.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /** 9 | * Represents the JS union `LineBreakStrategyIOS`, backed by a C++ enum. 10 | */ 11 | public typealias LineBreakStrategyIOS = margelo.nitro.nitrotext.LineBreakStrategyIOS 12 | 13 | public extension LineBreakStrategyIOS { 14 | /** 15 | * Get a LineBreakStrategyIOS for the given String value, or 16 | * return `nil` if the given value was invalid/unknown. 17 | */ 18 | init?(fromString string: String) { 19 | switch string { 20 | case "none": 21 | self = .none 22 | case "standard": 23 | self = .standard 24 | case "hangul-word": 25 | self = .hangulWord 26 | case "push-out": 27 | self = .pushOut 28 | default: 29 | return nil 30 | } 31 | } 32 | 33 | /** 34 | * Get the String value this LineBreakStrategyIOS represents. 35 | */ 36 | var stringValue: String { 37 | switch self { 38 | case .none: 39 | return "none" 40 | case .standard: 41 | return "standard" 42 | case .hangulWord: 43 | return "hangul-word" 44 | case .pushOut: 45 | return "push-out" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | **/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | *.keystore 35 | !debug.keystore 36 | .kotlin/ 37 | 38 | # node.js 39 | # 40 | node_modules/ 41 | npm-debug.log 42 | yarn-error.log 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | **/fastlane/report.xml 52 | **/fastlane/Preview.html 53 | **/fastlane/screenshots 54 | **/fastlane/test_output 55 | 56 | # Bundle artifact 57 | *.jsbundle 58 | 59 | # Ruby / CocoaPods 60 | **/Pods/ 61 | /vendor/bundle/ 62 | 63 | # Temporary files created by Metro to check the health of the file watcher 64 | .metro-health-check* 65 | 66 | # testing 67 | /coverage 68 | 69 | # Yarn 70 | .yarn/* 71 | !.yarn/patches 72 | !.yarn/plugins 73 | !.yarn/releases 74 | !.yarn/sdks 75 | !.yarn/versions 76 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-nitro-text-example", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios --simulator='iPhone 16'", 8 | "lint": "eslint .", 9 | "start": "react-native start --reset-cache", 10 | "test": "jest", 11 | "pod": "bundle install && bundle exec pod install --project-directory=ios" 12 | }, 13 | "dependencies": { 14 | "@react-navigation/bottom-tabs": "^7.8.5", 15 | "@react-navigation/native": "^7.1.20", 16 | "react": "19.1.0", 17 | "react-native": "0.81.4", 18 | "react-native-nitro-modules": "^0.31.9", 19 | "react-native-safe-area-context": "^5.5.2", 20 | "react-native-screens": "^4.18.0" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.25.2", 24 | "@babel/preset-env": "^7.25.3", 25 | "@babel/runtime": "^7.25.0", 26 | "@react-native-community/cli": "20.0.0", 27 | "@react-native-community/cli-platform-android": "20.0.0", 28 | "@react-native-community/cli-platform-ios": "20.0.0", 29 | "@react-native/babel-preset": "0.81.4", 30 | "@react-native/eslint-config": "0.81.4", 31 | "@react-native/metro-config": "0.81.4", 32 | "@react-native/typescript-config": "0.81.4", 33 | "@types/jest": "^29.5.13", 34 | "babel-plugin-module-resolver": "^5.0.2" 35 | }, 36 | "engines": { 37 | "node": ">=20" 38 | } 39 | } -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/TextDecorationLine.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextDecorationLine.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /** 9 | * Represents the JS union `TextDecorationLine`, backed by a C++ enum. 10 | */ 11 | public typealias TextDecorationLine = margelo.nitro.nitrotext.TextDecorationLine 12 | 13 | public extension TextDecorationLine { 14 | /** 15 | * Get a TextDecorationLine for the given String value, or 16 | * return `nil` if the given value was invalid/unknown. 17 | */ 18 | init?(fromString string: String) { 19 | switch string { 20 | case "none": 21 | self = .none 22 | case "underline": 23 | self = .underline 24 | case "line-through": 25 | self = .lineThrough 26 | case "underline line-through": 27 | self = .underlineLineThrough 28 | default: 29 | return nil 30 | } 31 | } 32 | 33 | /** 34 | * Get the String value this TextDecorationLine represents. 35 | */ 36 | var stringValue: String { 37 | switch self { 38 | case .none: 39 | return "none" 40 | case .underline: 41 | return "underline" 42 | case .lineThrough: 43 | return "line-through" 44 | case .underlineLineThrough: 45 | return "underline line-through" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/TextLayoutEvent.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextLayoutEvent.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | import Foundation 9 | import NitroModules 10 | 11 | /** 12 | * Represents an instance of `TextLayoutEvent`, backed by a C++ struct. 13 | */ 14 | public typealias TextLayoutEvent = margelo.nitro.nitrotext.TextLayoutEvent 15 | 16 | public extension TextLayoutEvent { 17 | private typealias bridge = margelo.nitro.nitrotext.bridge.swift 18 | 19 | /** 20 | * Create a new instance of `TextLayoutEvent`. 21 | */ 22 | init(lines: [TextLayout]) { 23 | self.init({ () -> bridge.std__vector_TextLayout_ in 24 | var __vector = bridge.create_std__vector_TextLayout_(lines.count) 25 | for __item in lines { 26 | __vector.push_back(__item) 27 | } 28 | return __vector 29 | }()) 30 | } 31 | 32 | var lines: [TextLayout] { 33 | @inline(__always) 34 | get { 35 | return self.__lines.map({ __item in __item }) 36 | } 37 | @inline(__always) 38 | set { 39 | self.__lines = { () -> bridge.std__vector_TextLayout_ in 40 | var __vector = bridge.create_std__vector_TextLayout_(newValue.count) 41 | for __item in newValue { 42 | __vector.push_back(__item) 43 | } 44 | return __vector 45 | }() 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/ios/NitroTextExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import React 3 | import React_RCTAppDelegate 4 | import ReactAppDependencyProvider 5 | 6 | @main 7 | class AppDelegate: UIResponder, UIApplicationDelegate { 8 | var window: UIWindow? 9 | 10 | var reactNativeDelegate: ReactNativeDelegate? 11 | var reactNativeFactory: RCTReactNativeFactory? 12 | 13 | func application( 14 | _ application: UIApplication, 15 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil 16 | ) -> Bool { 17 | let delegate = ReactNativeDelegate() 18 | let factory = RCTReactNativeFactory(delegate: delegate) 19 | delegate.dependencyProvider = RCTAppDependencyProvider() 20 | 21 | reactNativeDelegate = delegate 22 | reactNativeFactory = factory 23 | 24 | window = UIWindow(frame: UIScreen.main.bounds) 25 | 26 | factory.startReactNative( 27 | withModuleName: "NitroTextExample", 28 | in: window, 29 | launchOptions: launchOptions 30 | ) 31 | 32 | return true 33 | } 34 | } 35 | 36 | class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { 37 | override func sourceURL(for bridge: RCTBridge) -> URL? { 38 | self.bundleURL() 39 | } 40 | 41 | override func bundleURL() -> URL? { 42 | #if DEBUG 43 | RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") 44 | #else 45 | Bundle.main.url(forResource: "main", withExtension: "jsbundle") 46 | #endif 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/Func_void.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// Func_void.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | import Foundation 9 | import NitroModules 10 | 11 | /** 12 | * Wraps a Swift `() -> Void` as a class. 13 | * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. 14 | */ 15 | public final class Func_void { 16 | public typealias bridge = margelo.nitro.nitrotext.bridge.swift 17 | 18 | private let closure: () -> Void 19 | 20 | public init(_ closure: @escaping () -> Void) { 21 | self.closure = closure 22 | } 23 | 24 | @inline(__always) 25 | public func call() -> Void { 26 | self.closure() 27 | } 28 | 29 | /** 30 | * Casts this instance to a retained unsafe raw pointer. 31 | * This acquires one additional strong reference on the object! 32 | */ 33 | @inline(__always) 34 | public func toUnsafe() -> UnsafeMutableRawPointer { 35 | return Unmanaged.passRetained(self).toOpaque() 36 | } 37 | 38 | /** 39 | * Casts an unsafe pointer to a `Func_void`. 40 | * The pointer has to be a retained opaque `Unmanaged`. 41 | * This removes one strong reference from the object! 42 | */ 43 | @inline(__always) 44 | public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void { 45 | return Unmanaged.fromOpaque(pointer).takeRetainedValue() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Report a bug or unexpected behavior 3 | title: '[Bug]: ' 4 | labels: ['bug'] 5 | assignees: [] 6 | 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: What happened? 12 | description: A clear description of the bug 13 | placeholder: Describe the bug... 14 | validations: 15 | required: true 16 | 17 | - type: textarea 18 | id: steps 19 | attributes: 20 | label: Steps to reproduce 21 | description: How can we reproduce this? 22 | placeholder: | 23 | 1. Go to '...' 24 | 2. Click '....' 25 | 3. See error 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: code 31 | attributes: 32 | label: Code sample 33 | description: Minimal code that reproduces the issue 34 | placeholder: | 35 | ```tsx 36 | Your code here 37 | ``` 38 | render: tsx 39 | 40 | - type: input 41 | id: environment 42 | attributes: 43 | label: Environment 44 | description: React Native version, platform, device 45 | placeholder: e.g., RN 0.81.4, iOS, iPhone 15 Pro 46 | 47 | - type: checkboxes 48 | id: checklist 49 | attributes: 50 | label: Checklist 51 | options: 52 | - label: I have searched existing issues 53 | required: true 54 | - label: I have provided a minimal reproduction case 55 | required: true 56 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/nitrotextexample/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.nitrotextexample 2 | 3 | import android.app.Application 4 | import com.facebook.react.PackageList 5 | import com.facebook.react.ReactApplication 6 | import com.facebook.react.ReactHost 7 | import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative 8 | import com.facebook.react.ReactNativeHost 9 | import com.facebook.react.ReactPackage 10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 11 | import com.facebook.react.defaults.DefaultReactNativeHost 12 | 13 | class MainApplication : Application(), ReactApplication { 14 | 15 | override val reactNativeHost: ReactNativeHost = 16 | object : DefaultReactNativeHost(this) { 17 | override fun getPackages(): List = 18 | PackageList(this).packages.apply { 19 | // Packages that cannot be autolinked yet can be added manually here, for example: 20 | // add(MyReactNativePackage()) 21 | } 22 | 23 | override fun getJSMainModuleName(): String = "index" 24 | 25 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 26 | 27 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 28 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 29 | } 30 | 31 | override val reactHost: ReactHost 32 | get() = getDefaultReactHost(applicationContext, reactNativeHost) 33 | 34 | override fun onCreate() { 35 | super.onCreate() 36 | loadReactNative(this) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: read 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | release: 15 | name: Release 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | issues: write 20 | pull-requests: write 21 | id-token: write 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v6 25 | with: 26 | fetch-depth: 0 27 | - name: Setup Bun.js 28 | uses: oven-sh/setup-bun@v2 29 | with: 30 | bun-version: latest 31 | - name: Cache bun dependencies 32 | id: bun-cache 33 | uses: actions/cache@v4 34 | with: 35 | path: ~/.bun/install/cache 36 | key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-bun- 39 | 40 | - name: Install npm dependencies (bun) 41 | run: bun install 42 | 43 | - name: Build lib 44 | run: bun run build 45 | 46 | - name: Release 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 50 | NPM_CONFIG_PROVENANCE: true 51 | GIT_AUTHOR_NAME: ${{ github.actor }} 52 | GIT_AUTHOR_EMAIL: "${{ github.actor }}@users.noreply.github.com" 53 | GIT_COMMITTER_NAME: ${{ github.actor }} 54 | GIT_COMMITTER_EMAIL: "${{ github.actor }}@users.noreply.github.com" 55 | run: bun release 56 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/MenuItem.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// MenuItem.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | import Foundation 9 | import NitroModules 10 | 11 | /** 12 | * Represents an instance of `MenuItem`, backed by a C++ struct. 13 | */ 14 | public typealias MenuItem = margelo.nitro.nitrotext.MenuItem 15 | 16 | public extension MenuItem { 17 | private typealias bridge = margelo.nitro.nitrotext.bridge.swift 18 | 19 | /** 20 | * Create a new instance of `MenuItem`. 21 | */ 22 | init(title: String, action: @escaping () -> Void) { 23 | self.init(std.string(title), { () -> bridge.Func_void in 24 | let __closureWrapper = Func_void(action) 25 | return bridge.create_Func_void(__closureWrapper.toUnsafe()) 26 | }()) 27 | } 28 | 29 | var title: String { 30 | @inline(__always) 31 | get { 32 | return String(self.__title) 33 | } 34 | @inline(__always) 35 | set { 36 | self.__title = std.string(newValue) 37 | } 38 | } 39 | 40 | var action: () -> Void { 41 | @inline(__always) 42 | get { 43 | return { () -> () -> Void in 44 | let __wrappedFunction = bridge.wrap_Func_void(self.__action) 45 | return { () -> Void in 46 | __wrappedFunction.call() 47 | } 48 | }() 49 | } 50 | @inline(__always) 51 | set { 52 | self.__action = { () -> bridge.Func_void in 53 | let __closureWrapper = Func_void(newValue) 54 | return bridge.create_Func_void(__closureWrapper.toUnsafe()) 55 | }() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/Func_void_TextLayoutEvent.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// Func_void_TextLayoutEvent.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | import Foundation 9 | import NitroModules 10 | 11 | /** 12 | * Wraps a Swift `(_ layout: TextLayoutEvent) -> Void` as a class. 13 | * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. 14 | */ 15 | public final class Func_void_TextLayoutEvent { 16 | public typealias bridge = margelo.nitro.nitrotext.bridge.swift 17 | 18 | private let closure: (_ layout: TextLayoutEvent) -> Void 19 | 20 | public init(_ closure: @escaping (_ layout: TextLayoutEvent) -> Void) { 21 | self.closure = closure 22 | } 23 | 24 | @inline(__always) 25 | public func call(layout: TextLayoutEvent) -> Void { 26 | self.closure(layout) 27 | } 28 | 29 | /** 30 | * Casts this instance to a retained unsafe raw pointer. 31 | * This acquires one additional strong reference on the object! 32 | */ 33 | @inline(__always) 34 | public func toUnsafe() -> UnsafeMutableRawPointer { 35 | return Unmanaged.passRetained(self).toOpaque() 36 | } 37 | 38 | /** 39 | * Casts an unsafe pointer to a `Func_void_TextLayoutEvent`. 40 | * The pointer has to be a retained opaque `Unmanaged`. 41 | * This removes one strong reference from the object! 42 | */ 43 | @inline(__always) 44 | public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_TextLayoutEvent { 45 | return Unmanaged.fromOpaque(pointer).takeRetainedValue() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/ios/NitroTextExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | NitroTextExample 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSAllowsLocalNetworking 32 | 33 | 34 | NSLocationWhenInUseUsageDescription 35 | 36 | RCTNewArchEnabled 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | arm64 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | import { NavigationContainer } from '@react-navigation/native'; 4 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 5 | import { PlainTextScreen } from './screens/PlainTextScreen'; 6 | import { HtmlScreen } from './screens/HtmlScreen'; 7 | import { PerformanceScreen } from './screens/PerformanceScreen'; 8 | 9 | const Tab = createBottomTabNavigator(); 10 | 11 | function MyTabs() { 12 | return ( 13 | 20 | ( 26 | 📄 27 | ), 28 | }} 29 | /> 30 | ( 36 | 🌐 37 | ), 38 | }} 39 | /> 40 | ( 46 | 47 | ), 48 | }} 49 | /> 50 | 51 | ); 52 | } 53 | 54 | export default function App() { 55 | return ( 56 | 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /cpp/NitroTextShadowNode.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // NitroTextShadowNode.hpp 3 | // Custom, non-generated ShadowNode for NitroText 4 | // 5 | 6 | #pragma once 7 | 8 | #include "HybridNitroTextComponent.hpp" 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace margelo::nitro::nitrotext::views { 20 | 21 | /** 22 | * The Shadow Node for the "NitroText" View. 23 | * Mark as a Leaf + Measurable Yoga node so Fabric queries the ShadowNode for 24 | * size. (We measure cross-platform in C++ using TextLayoutManager, like 25 | * Paragraph.) 26 | */ 27 | class NitroTextShadowNode final 28 | : public react::ConcreteViewShadowNode< 29 | HybridNitroTextComponentName, 30 | HybridNitroTextProps, 31 | react::ViewEventEmitter, 32 | HybridNitroTextState> { 33 | public: 34 | using ConcreteViewShadowNode::ConcreteViewShadowNode; 35 | 36 | static react::ShadowNodeTraits BaseTraits(); 37 | 38 | void setTextLayoutManager( 39 | std::shared_ptr tlm); 40 | 41 | protected: 42 | react::Size measureContent( 43 | const react::LayoutContext &layoutContext, 44 | const react::LayoutConstraints &layoutConstraints) const override; 45 | 46 | react::Float baseline(const react::LayoutContext &layoutContext, 47 | react::Size size) const override; 48 | 49 | private: 50 | std::shared_ptr textLayoutManager_; 51 | }; 52 | 53 | } // namespace margelo::nitro::nitrotext::views 54 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/DynamicTypeRamp.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// DynamicTypeRamp.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /** 9 | * Represents the JS union `DynamicTypeRamp`, backed by a C++ enum. 10 | */ 11 | public typealias DynamicTypeRamp = margelo.nitro.nitrotext.DynamicTypeRamp 12 | 13 | public extension DynamicTypeRamp { 14 | /** 15 | * Get a DynamicTypeRamp for the given String value, or 16 | * return `nil` if the given value was invalid/unknown. 17 | */ 18 | init?(fromString string: String) { 19 | switch string { 20 | case "caption2": 21 | self = .caption2 22 | case "caption1": 23 | self = .caption1 24 | case "footnote": 25 | self = .footnote 26 | case "subheadline": 27 | self = .subheadline 28 | case "callout": 29 | self = .callout 30 | case "body": 31 | self = .body 32 | case "headline": 33 | self = .headline 34 | case "title3": 35 | self = .title3 36 | case "title2": 37 | self = .title2 38 | case "title1": 39 | self = .title1 40 | case "largeTitle": 41 | self = .largetitle 42 | default: 43 | return nil 44 | } 45 | } 46 | 47 | /** 48 | * Get the String value this DynamicTypeRamp represents. 49 | */ 50 | var stringValue: String { 51 | switch self { 52 | case .caption2: 53 | return "caption2" 54 | case .caption1: 55 | return "caption1" 56 | case .footnote: 57 | return "footnote" 58 | case .subheadline: 59 | return "subheadline" 60 | case .callout: 61 | return "callout" 62 | case .body: 63 | return "body" 64 | case .headline: 65 | return "headline" 66 | case .title3: 67 | return "title3" 68 | case .title2: 69 | return "title2" 70 | case .title1: 71 | return "title1" 72 | case .largetitle: 73 | return "largeTitle" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Use this property to specify which architecture you want to build. 26 | # You can also override it from the CLI using 27 | # ./gradlew -PreactNativeArchitectures=x86_64 28 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 29 | 30 | # Use this property to enable support to the new architecture. 31 | # This will allow you to use TurboModules and the Fabric render in 32 | # your application. You should enable this flag either if you want 33 | # to write custom TurboModules/Fabric components OR use libraries that 34 | # are providing them. 35 | newArchEnabled=true 36 | 37 | # Use this property to enable or disable the Hermes JS engine. 38 | # If set to false, you will be using JSC instead. 39 | hermesEnabled=true 40 | 41 | # Use this property to enable edge-to-edge display support. 42 | # This allows your app to draw behind system bars for an immersive UI. 43 | # Note: Only works with ReactActivity and should not be used with custom Activity. 44 | edgeToEdgeEnabled=false 45 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/FontWeight.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// FontWeight.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /** 9 | * Represents the JS union `FontWeight`, backed by a C++ enum. 10 | */ 11 | public typealias FontWeight = margelo.nitro.nitrotext.FontWeight 12 | 13 | public extension FontWeight { 14 | /** 15 | * Get a FontWeight for the given String value, or 16 | * return `nil` if the given value was invalid/unknown. 17 | */ 18 | init?(fromString string: String) { 19 | switch string { 20 | case "normal": 21 | self = .normal 22 | case "bold": 23 | self = .bold 24 | case "ultralight": 25 | self = .ultralight 26 | case "thin": 27 | self = .thin 28 | case "light": 29 | self = .light 30 | case "medium": 31 | self = .medium 32 | case "regular": 33 | self = .regular 34 | case "semibold": 35 | self = .semibold 36 | case "condensedBold": 37 | self = .condensedbold 38 | case "condensed": 39 | self = .condensed 40 | case "heavy": 41 | self = .heavy 42 | case "black": 43 | self = .black 44 | default: 45 | return nil 46 | } 47 | } 48 | 49 | /** 50 | * Get the String value this FontWeight represents. 51 | */ 52 | var stringValue: String { 53 | switch self { 54 | case .normal: 55 | return "normal" 56 | case .bold: 57 | return "bold" 58 | case .ultralight: 59 | return "ultralight" 60 | case .thin: 61 | return "thin" 62 | case .light: 63 | return "light" 64 | case .medium: 65 | return "medium" 66 | case .regular: 67 | return "regular" 68 | case .semibold: 69 | return "semibold" 70 | case .condensedbold: 71 | return "condensedBold" 72 | case .condensed: 73 | return "condensed" 74 | case .heavy: 75 | return "heavy" 76 | case .black: 77 | return "black" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ios/HybridNitroTextComponentOverride.mm: -------------------------------------------------------------------------------- 1 | // 2 | // NitroTextComponentOverride.mm 3 | // Override ComponentDescriptor and ComponentView behavior for the generated view class 4 | // without introducing a new ComponentView class. 5 | // 6 | 7 | #import 8 | #import 9 | #import 10 | #import 11 | 12 | #import "NitroTextComponentDescriptor.hpp" 13 | 14 | // Forward-declare the generated view class; we don't import generated headers here. 15 | @interface HybridNitroTextComponent : RCTViewComponentView 16 | @end 17 | 18 | using namespace facebook; 19 | using namespace margelo::nitro::nitrotext::views; 20 | 21 | @interface HybridNitroTextComponent (ComponentDescriptorOverride) 22 | @end 23 | 24 | @implementation HybridNitroTextComponent (ComponentDescriptorOverride) 25 | 26 | + (void)load 27 | { 28 | NSLog(@"[NitroText] ComponentDescriptorOverride Step 1: +load - Re-registering HybridNitroTextComponent"); 29 | 30 | // Re-register THIS class so the factory picks up our overridden methods below. 31 | // This MUST happen first to ensure our overrides are respected. 32 | // Without this, the generated class's methods would be used instead. 33 | [[RCTComponentViewFactory currentComponentViewFactory] registerComponentViewClass:self]; 34 | } 35 | 36 | + (react::ComponentDescriptorProvider)componentDescriptorProvider 37 | { 38 | NSLog(@"[NitroText] ComponentDescriptorOverride Step 2: Providing custom ComponentDescriptorProvider"); 39 | 40 | // Return our custom descriptor (which uses our custom ShadowNode). 41 | // This is critical for proper layout and measurement. 42 | return react::concreteComponentDescriptorProvider(); 43 | } 44 | 45 | + (BOOL)shouldBeRecycled 46 | { 47 | NSLog(@"[NitroText] ComponentOverride Step 3: Disabling component recycling"); 48 | 49 | // Disable component recycling for NitroText to ensure proper cleanup and re-initialization. 50 | // This prevents recycling which was causing issues with attributed text state. 51 | return NO; 52 | } 53 | 54 | 55 | @end 56 | 57 | -------------------------------------------------------------------------------- /release.config.cjs: -------------------------------------------------------------------------------- 1 | const rules = [ 2 | { type: 'feat', release: 'minor', title: '✨ Features' }, 3 | { type: 'fix', release: 'patch', title: '🐛 Bug Fixes' }, 4 | { type: 'perf', release: 'patch', title: '💨 Performance Improvements' }, 5 | { type: 'refactor', release: 'patch', title: '🔄 Code Refactors' }, 6 | { type: 'docs', release: 'patch', title: '📚 Documentation' }, 7 | { type: 'chore', release: 'patch', title: '🛠️ Other changes' }, 8 | ] 9 | 10 | const sortMap = Object.fromEntries( 11 | rules.map((rule, index) => [rule.title, index]) 12 | ) 13 | 14 | /** 15 | * @type {import('semantic-release').GlobalConfig} 16 | */ 17 | module.exports = { 18 | branches: ['main', { name: 'next', prerelease: 'next' }], 19 | plugins: [ 20 | [ 21 | '@semantic-release/commit-analyzer', 22 | { 23 | preset: 'conventionalcommits', 24 | releaseRules: [ 25 | { breaking: true, release: 'major' }, 26 | { revert: true, release: 'patch' }, 27 | ].concat(rules.map(({ type, release }) => ({ type, release }))), 28 | }, 29 | ], 30 | [ 31 | '@semantic-release/release-notes-generator', 32 | { 33 | preset: 'conventionalcommits', 34 | presetConfig: { 35 | types: rules.map(({ type, title }) => ({ 36 | type, 37 | section: title, 38 | })), 39 | }, 40 | writerOpts: { 41 | commitGroupsSort: (a, z) => 42 | sortMap[a.title] - sortMap[z.title], 43 | }, 44 | }, 45 | ], 46 | [ 47 | '@semantic-release/changelog', 48 | { 49 | changelogFile: 'CHANGELOG.md', 50 | }, 51 | ], 52 | '@semantic-release/npm', 53 | '@semantic-release/github', 54 | [ 55 | '@semantic-release/git', 56 | { 57 | assets: ['package.json', 'CHANGELOG.md', 'example/package.json'], 58 | }, 59 | ], 60 | ], 61 | } 62 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/NitroText+autolinking.rb: -------------------------------------------------------------------------------- 1 | # 2 | # NitroText+autolinking.rb 3 | # This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | # https://github.com/mrousavy/nitro 5 | # Copyright © 2025 Marc Rousavy @ Margelo 6 | # 7 | 8 | # This is a Ruby script that adds all files generated by Nitrogen 9 | # to the given podspec. 10 | # 11 | # To use it, add this to your .podspec: 12 | # ```ruby 13 | # Pod::Spec.new do |spec| 14 | # # ... 15 | # 16 | # # Add all files generated by Nitrogen 17 | # load 'nitrogen/generated/ios/NitroText+autolinking.rb' 18 | # add_nitrogen_files(spec) 19 | # end 20 | # ``` 21 | 22 | def add_nitrogen_files(spec) 23 | Pod::UI.puts "[NitroModules] 🔥 NitroText is boosted by nitro!" 24 | 25 | spec.dependency "NitroModules" 26 | 27 | current_source_files = Array(spec.attributes_hash['source_files']) 28 | spec.source_files = current_source_files + [ 29 | # Generated cross-platform specs 30 | "nitrogen/generated/shared/**/*.{h,hpp,c,cpp,swift}", 31 | # Generated bridges for the cross-platform specs 32 | "nitrogen/generated/ios/**/*.{h,hpp,c,cpp,mm,swift}", 33 | ] 34 | 35 | current_public_header_files = Array(spec.attributes_hash['public_header_files']) 36 | spec.public_header_files = current_public_header_files + [ 37 | # Generated specs 38 | "nitrogen/generated/shared/**/*.{h,hpp}", 39 | # Swift to C++ bridging helpers 40 | "nitrogen/generated/ios/NitroText-Swift-Cxx-Bridge.hpp" 41 | ] 42 | 43 | current_private_header_files = Array(spec.attributes_hash['private_header_files']) 44 | spec.private_header_files = current_private_header_files + [ 45 | # iOS specific specs 46 | "nitrogen/generated/ios/c++/**/*.{h,hpp}", 47 | # Views are framework-specific and should be private 48 | "nitrogen/generated/shared/**/views/**/*" 49 | ] 50 | 51 | current_pod_target_xcconfig = spec.attributes_hash['pod_target_xcconfig'] || {} 52 | spec.pod_target_xcconfig = current_pod_target_xcconfig.merge({ 53 | # Use C++ 20 54 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", 55 | # Enables C++ <-> Swift interop (by default it's only C) 56 | "SWIFT_OBJC_INTEROP_MODE" => "objcxx", 57 | # Enables stricter modular headers 58 | "DEFINES_MODULE" => "YES", 59 | }) 60 | end 61 | -------------------------------------------------------------------------------- /ios/NitroTextImpl+Paragraph.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NitroTextImpl+Paragraph.swift 3 | // Pods 4 | // 5 | // Created by Patrick Kabwe on 14/09/2025. 6 | // 7 | 8 | import UIKit 9 | 10 | struct ParagraphKey: Hashable { 11 | let lineHeight: CGFloat 12 | let alignmentRaw: Int 13 | let strategyRaw: UInt 14 | } 15 | 16 | extension NitroTextImpl { 17 | 18 | func makeParagraphStyle(for fragment: Fragment) -> NSParagraphStyle { 19 | let textAlignment: NSTextAlignment = { 20 | if let align = fragment.textAlign { 21 | switch align { 22 | case .center: return .center 23 | case .right: return .right 24 | case .justify: return .justified 25 | case .left: return .left 26 | case .auto: return .natural 27 | } 28 | } 29 | return currentTextAlignment 30 | }() 31 | 32 | var _lineHeight: CGFloat = 0 33 | if let lineHeight = fragment.lineHeight, lineHeight > 0 { 34 | let baseSize: CGFloat = fragment.fontSize.map({ CGFloat($0) }) 35 | ?? nitroTextView?.font?.pointSize 36 | ?? 14.0 37 | let scale = allowFontScaling ? getScaleFactor(requestedSize: baseSize) : 1.0 38 | _lineHeight = CGFloat(lineHeight) * scale 39 | } 40 | 41 | let strategyRaw: UInt = { 42 | if #available(iOS 14.0, *) { 43 | return currentLineBreakStrategy.rawValue 44 | } 45 | return 0 46 | }() 47 | 48 | let key = ParagraphKey( 49 | lineHeight: _lineHeight, 50 | alignmentRaw: Int(textAlignment.rawValue), 51 | strategyRaw: strategyRaw 52 | ) 53 | if let cached = paragraphStyleCache[key] { return cached } 54 | 55 | let para = NSMutableParagraphStyle() 56 | if _lineHeight > 0 { 57 | para.minimumLineHeight = _lineHeight 58 | para.maximumLineHeight = _lineHeight 59 | } 60 | para.alignment = textAlignment 61 | if #available(iOS 14.0, *) { 62 | para.lineBreakStrategy = currentLineBreakStrategy 63 | } 64 | 65 | let immutable = para.copy() as! NSParagraphStyle 66 | paragraphStyleCache[key] = immutable 67 | return immutable 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/android-build.yml: -------------------------------------------------------------------------------- 1 | name: Build Android 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - '.github/workflows/android-build.yml' 12 | - 'example/android/**' 13 | - 'nitrogen/generated/shared/**' 14 | - 'nitrogen/generated/android/**' 15 | - 'cpp/**' 16 | - 'android/**' 17 | - '**/bun.lock' 18 | - '**/react-native.config.js' 19 | - '**/nitro.json' 20 | pull_request: 21 | paths: 22 | - '.github/workflows/android-build.yml' 23 | - 'example/android/**' 24 | - '**/nitrogen/generated/shared/**' 25 | - '**/nitrogen/generated/android/**' 26 | - 'cpp/**' 27 | - 'android/**' 28 | - '**/bun.lock' 29 | - '**/react-native.config.js' 30 | - '**/nitro.json' 31 | workflow_dispatch: 32 | 33 | concurrency: 34 | group: ${{ github.workflow }}-${{ github.ref }} 35 | cancel-in-progress: true 36 | 37 | jobs: 38 | build: 39 | name: Build Android Example App (${{ matrix.arch }}) 40 | runs-on: ubuntu-latest 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | arch: [new, old] 45 | steps: 46 | - uses: actions/checkout@v6 47 | - uses: oven-sh/setup-bun@v2 48 | 49 | - name: Install dependencies (bun) 50 | run: bun install 51 | 52 | - name: Disable new architecture in gradle.properties 53 | if: matrix.arch == 'old' 54 | run: sed -i "s/newArchEnabled=true/newArchEnabled=false/g" example/android/gradle.properties 55 | 56 | - name: Setup JDK 17 57 | uses: actions/setup-java@v5 58 | with: 59 | distribution: 'zulu' 60 | java-version: '17' 61 | cache: 'gradle' 62 | 63 | - name: Cache Gradle 64 | uses: actions/cache@v4 65 | with: 66 | path: | 67 | ~/.gradle/caches 68 | ~/.gradle/wrapper 69 | key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/**/*.gradle*') }} 70 | restore-keys: | 71 | ${{ runner.os }}-gradle- 72 | 73 | - name: Run Gradle build 74 | working-directory: example/android 75 | run: ./gradlew assembleDebug --no-daemon --build-cache 76 | 77 | - name: Stop Gradle daemon 78 | working-directory: example/android 79 | run: ./gradlew --stop 80 | -------------------------------------------------------------------------------- /cpp/NitroTextComponentDescriptor.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // NitroTextComponentDescriptor.cpp 3 | // Shared implementation for custom ComponentDescriptor 4 | // 5 | 6 | #include "NitroTextComponentDescriptor.hpp" 7 | #include 8 | 9 | using namespace facebook; 10 | using namespace margelo::nitro::nitrotext::views; 11 | 12 | NitroTextComponentDescriptor::NitroTextComponentDescriptor(const react::ComponentDescriptorParameters& parameters) 13 | : ConcreteComponentDescriptor(parameters, 14 | react::RawPropsParser(/* enableJsiParser */ true)) {} 15 | 16 | std::shared_ptr NitroTextComponentDescriptor::cloneProps(const react::PropsParserContext& context, 17 | const std::shared_ptr& props, 18 | react::RawProps rawProps) const { 19 | // 1. Prepare raw props parser 20 | rawProps.parse(rawPropsParser_); 21 | // 2. Copy props with Nitro's cached copy constructor 22 | return NitroTextShadowNode::Props(context, /* & */ rawProps, props); 23 | } 24 | 25 | void NitroTextComponentDescriptor::adopt(react::ShadowNode& shadowNode) const { 26 | // Always call base adopt first. 27 | ConcreteComponentDescriptor::adopt(shadowNode); 28 | 29 | auto& concreteShadowNode = dynamic_cast(shadowNode); 30 | 31 | #ifdef ANDROID 32 | // On Android, wrap props into state for JNI roundtrip. 33 | const HybridNitroTextProps& props = concreteShadowNode.getConcreteProps(); 34 | HybridNitroTextState state; 35 | state.setProps(props); 36 | concreteShadowNode.setStateData(std::move(state)); 37 | #endif 38 | 39 | // Inject TextLayoutManager so measurement works on Fabric (iOS/macOS/etc.). 40 | // Construct directly with the descriptor's ContextContainer. 41 | if (auto contextContainer = this->getContextContainer()) { 42 | auto textLayoutManager = std::make_shared(contextContainer); 43 | concreteShadowNode.setTextLayoutManager(textLayoutManager); 44 | } else { 45 | auto textLayoutManager = std::make_shared(nullptr); 46 | concreteShadowNode.setTextLayoutManager(textLayoutManager); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/NitroText-Swift-Cxx-Bridge.cpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// NitroText-Swift-Cxx-Bridge.cpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #include "NitroText-Swift-Cxx-Bridge.hpp" 9 | 10 | // Include C++ implementation defined types 11 | #include "HybridNitroTextSpecSwift.hpp" 12 | #include "NitroText-Swift-Cxx-Umbrella.hpp" 13 | #include 14 | 15 | namespace margelo::nitro::nitrotext::bridge::swift { 16 | 17 | // pragma MARK: std::function 18 | Func_void create_Func_void(void* NON_NULL swiftClosureWrapper) noexcept { 19 | auto swiftClosure = NitroText::Func_void::fromUnsafe(swiftClosureWrapper); 20 | return [swiftClosure = std::move(swiftClosure)]() mutable -> void { 21 | swiftClosure.call(); 22 | }; 23 | } 24 | 25 | // pragma MARK: std::function 26 | Func_void_TextLayoutEvent create_Func_void_TextLayoutEvent(void* NON_NULL swiftClosureWrapper) noexcept { 27 | auto swiftClosure = NitroText::Func_void_TextLayoutEvent::fromUnsafe(swiftClosureWrapper); 28 | return [swiftClosure = std::move(swiftClosure)](const TextLayoutEvent& layout) mutable -> void { 29 | swiftClosure.call(layout); 30 | }; 31 | } 32 | 33 | // pragma MARK: std::shared_ptr 34 | std::shared_ptr create_std__shared_ptr_HybridNitroTextSpec_(void* NON_NULL swiftUnsafePointer) noexcept { 35 | NitroText::HybridNitroTextSpec_cxx swiftPart = NitroText::HybridNitroTextSpec_cxx::fromUnsafe(swiftUnsafePointer); 36 | return std::make_shared(swiftPart); 37 | } 38 | void* NON_NULL get_std__shared_ptr_HybridNitroTextSpec_(std__shared_ptr_HybridNitroTextSpec_ cppType) { 39 | std::shared_ptr swiftWrapper = std::dynamic_pointer_cast(cppType); 40 | #ifdef NITRO_DEBUG 41 | if (swiftWrapper == nullptr) [[unlikely]] { 42 | throw std::runtime_error("Class \"HybridNitroTextSpec\" is not implemented in Swift!"); 43 | } 44 | #endif 45 | NitroText::HybridNitroTextSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); 46 | return swiftPart.toUnsafe(); 47 | } 48 | 49 | } // namespace margelo::nitro::nitrotext::bridge::swift 50 | -------------------------------------------------------------------------------- /cpp/NitroTextLogger.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // NitroTextLogger.hpp 3 | // 4 | // 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | // Check if we're in debug mode 13 | #ifndef NDEBUG 14 | #define NITRO_TEXT_DEBUG_LOGGING 1 15 | #else 16 | #define NITRO_TEXT_DEBUG_LOGGING 0 17 | #endif 18 | 19 | namespace margelo::nitro::nitrotext::logger { 20 | 21 | /** 22 | * @brief Logs an informational message 23 | * @param message The message to log 24 | * @param tag Optional tag identifier for categorization 25 | * 26 | * Only logs in debug builds to avoid performance overhead in production 27 | */ 28 | inline void info( 29 | const std::string& message, 30 | const std::string& tag = "") { 31 | #if NITRO_TEXT_DEBUG_LOGGING 32 | std::ostringstream oss; 33 | oss << "[NitroText]"; 34 | if (!tag.empty()) { 35 | oss << " [" << tag << "]"; 36 | } 37 | oss << " " << message; 38 | std::cout << oss.str() << std::endl; 39 | #else 40 | // No-op in release builds 41 | (void)message; 42 | (void)tag; 43 | #endif 44 | } 45 | 46 | /** 47 | * @brief Logs a warning message 48 | * @param message The warning message to log 49 | * @param tag Optional tag identifier for categorization 50 | * 51 | * Only logs in debug builds to avoid performance overhead in production 52 | */ 53 | inline void warn( 54 | const std::string& message, 55 | const std::string& tag = "") { 56 | #if NITRO_TEXT_DEBUG_LOGGING 57 | std::ostringstream oss; 58 | oss << "[NitroText] [WARN]"; 59 | if (!tag.empty()) { 60 | oss << " [" << tag << "]"; 61 | } 62 | oss << " " << message; 63 | std::cerr << oss.str() << std::endl; 64 | #else 65 | // No-op in release builds 66 | (void)message; 67 | (void)tag; 68 | #endif 69 | } 70 | 71 | /** 72 | * @brief Logs an error message 73 | * @param message The error message to log 74 | * @param tag Optional tag identifier for categorization 75 | * 76 | * Only logs in debug builds to avoid performance overhead in production 77 | */ 78 | inline void error( 79 | const std::string& message, 80 | const std::string& tag = "") { 81 | #if NITRO_TEXT_DEBUG_LOGGING 82 | std::ostringstream oss; 83 | oss << "[NitroText] [ERROR]"; 84 | if (!tag.empty()) { 85 | oss << " [" << tag << "]"; 86 | } 87 | oss << " " << message; 88 | std::cerr << oss.str() << std::endl; 89 | #else 90 | // No-op in release builds 91 | (void)message; 92 | (void)tag; 93 | #endif 94 | } 95 | 96 | } // namespace margelo::nitro::nitrotext::logger 97 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import eslint from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | import react from 'eslint-plugin-react'; 5 | import reactHooks from 'eslint-plugin-react-hooks'; 6 | import reactNative from 'eslint-plugin-react-native'; 7 | import prettierPlugin from 'eslint-plugin-prettier'; 8 | import prettierConfig from 'eslint-config-prettier'; 9 | import nitroModulesPlugin from 'eslint-plugin-nitro-modules'; 10 | 11 | export default defineConfig([ 12 | eslint.configs.recommended, 13 | nitroModulesPlugin.configs.recommended, 14 | ...tseslint.configs.recommended, 15 | prettierConfig, 16 | { 17 | files: ['src/**/*.{js,jsx,ts,tsx}'], 18 | languageOptions: { 19 | ecmaVersion: 2024, 20 | sourceType: 'module', 21 | parserOptions: { 22 | ecmaFeatures: { 23 | jsx: true, 24 | }, 25 | }, 26 | }, 27 | plugins: { 28 | react, 29 | 'react-hooks': reactHooks, 30 | 'react-native': reactNative, 31 | prettier: prettierPlugin 32 | }, 33 | rules: { 34 | ...react.configs['jsx-runtime'].rules, 35 | ...reactHooks.configs.recommended.rules, 36 | 'react/react-in-jsx-scope': 'off', 37 | 'react/prop-types': 'off', 38 | '@typescript-eslint/no-explicit-any': 'warn', 39 | "@typescript-eslint/no-empty-object-type": "off", 40 | "@typescript-eslint/no-require-imports": "off", 41 | '@typescript-eslint/no-unused-vars': [ 42 | 'warn', 43 | { 44 | argsIgnorePattern: '^_', 45 | varsIgnorePattern: '^_', 46 | }, 47 | ], 48 | 'react-hooks/rules-of-hooks': 'error', 49 | 'react-hooks/exhaustive-deps': 'warn', 50 | 51 | 'prettier/prettier': [ 52 | 'warn', 53 | { 54 | "quoteProps": "consistent", 55 | "singleQuote": true, 56 | "tabWidth": 3, 57 | "trailingComma": "es5", 58 | "useTabs": false, 59 | "semi": false 60 | }, 61 | ], 62 | }, 63 | settings: { 64 | react: { 65 | version: 'detect', 66 | }, 67 | }, 68 | }, 69 | { 70 | ignores: [ 71 | 'node_modules/**', 72 | 'lib/**', 73 | 'build/**', 74 | 'example/**', 75 | 'nitrogen/generated/**', 76 | 'android/**', 77 | 'ios/**', 78 | 'cpp/**', 79 | '*.config.js', 80 | '*.config.mjs', 81 | '*.config.cjs', 82 | '*.podspec', 83 | ], 84 | }, 85 | ]); 86 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/TextLayout.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextLayout.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | import Foundation 9 | import NitroModules 10 | 11 | /** 12 | * Represents an instance of `TextLayout`, backed by a C++ struct. 13 | */ 14 | public typealias TextLayout = margelo.nitro.nitrotext.TextLayout 15 | 16 | public extension TextLayout { 17 | private typealias bridge = margelo.nitro.nitrotext.bridge.swift 18 | 19 | /** 20 | * Create a new instance of `TextLayout`. 21 | */ 22 | init(text: String, x: Double, y: Double, width: Double, height: Double, descender: Double, capHeight: Double, ascender: Double, xHeight: Double) { 23 | self.init(std.string(text), x, y, width, height, descender, capHeight, ascender, xHeight) 24 | } 25 | 26 | var text: String { 27 | @inline(__always) 28 | get { 29 | return String(self.__text) 30 | } 31 | @inline(__always) 32 | set { 33 | self.__text = std.string(newValue) 34 | } 35 | } 36 | 37 | var x: Double { 38 | @inline(__always) 39 | get { 40 | return self.__x 41 | } 42 | @inline(__always) 43 | set { 44 | self.__x = newValue 45 | } 46 | } 47 | 48 | var y: Double { 49 | @inline(__always) 50 | get { 51 | return self.__y 52 | } 53 | @inline(__always) 54 | set { 55 | self.__y = newValue 56 | } 57 | } 58 | 59 | var width: Double { 60 | @inline(__always) 61 | get { 62 | return self.__width 63 | } 64 | @inline(__always) 65 | set { 66 | self.__width = newValue 67 | } 68 | } 69 | 70 | var height: Double { 71 | @inline(__always) 72 | get { 73 | return self.__height 74 | } 75 | @inline(__always) 76 | set { 77 | self.__height = newValue 78 | } 79 | } 80 | 81 | var descender: Double { 82 | @inline(__always) 83 | get { 84 | return self.__descender 85 | } 86 | @inline(__always) 87 | set { 88 | self.__descender = newValue 89 | } 90 | } 91 | 92 | var capHeight: Double { 93 | @inline(__always) 94 | get { 95 | return self.__capHeight 96 | } 97 | @inline(__always) 98 | set { 99 | self.__capHeight = newValue 100 | } 101 | } 102 | 103 | var ascender: Double { 104 | @inline(__always) 105 | get { 106 | return self.__ascender 107 | } 108 | @inline(__always) 109 | set { 110 | self.__ascender = newValue 111 | } 112 | } 113 | 114 | var xHeight: Double { 115 | @inline(__always) 116 | get { 117 | return self.__xHeight 118 | } 119 | @inline(__always) 120 | set { 121 | self.__xHeight = newValue 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/TextLayoutEvent.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextLayoutEvent.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | // Forward declaration of `TextLayout` to properly resolve imports. 27 | namespace margelo::nitro::nitrotext { struct TextLayout; } 28 | 29 | #include "TextLayout.hpp" 30 | #include 31 | 32 | namespace margelo::nitro::nitrotext { 33 | 34 | /** 35 | * A struct which can be represented as a JavaScript object (TextLayoutEvent). 36 | */ 37 | struct TextLayoutEvent { 38 | public: 39 | std::vector lines SWIFT_PRIVATE; 40 | 41 | public: 42 | TextLayoutEvent() = default; 43 | explicit TextLayoutEvent(std::vector lines): lines(lines) {} 44 | }; 45 | 46 | } // namespace margelo::nitro::nitrotext 47 | 48 | namespace margelo::nitro { 49 | 50 | // C++ TextLayoutEvent <> JS TextLayoutEvent (object) 51 | template <> 52 | struct JSIConverter final { 53 | static inline margelo::nitro::nitrotext::TextLayoutEvent fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 54 | jsi::Object obj = arg.asObject(runtime); 55 | return margelo::nitro::nitrotext::TextLayoutEvent( 56 | JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "lines")) 57 | ); 58 | } 59 | static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitrotext::TextLayoutEvent& arg) { 60 | jsi::Object obj(runtime); 61 | obj.setProperty(runtime, "lines", JSIConverter>::toJSI(runtime, arg.lines)); 62 | return obj; 63 | } 64 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 65 | if (!value.isObject()) { 66 | return false; 67 | } 68 | jsi::Object obj = value.getObject(runtime); 69 | if (!nitro::isPlainObject(runtime, obj)) { 70 | return false; 71 | } 72 | if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "lines"))) return false; 73 | return true; 74 | } 75 | }; 76 | 77 | } // namespace margelo::nitro 78 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/MenuItem.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// MenuItem.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | 27 | 28 | #include 29 | #include 30 | 31 | namespace margelo::nitro::nitrotext { 32 | 33 | /** 34 | * A struct which can be represented as a JavaScript object (MenuItem). 35 | */ 36 | struct MenuItem { 37 | public: 38 | std::string title SWIFT_PRIVATE; 39 | std::function action SWIFT_PRIVATE; 40 | 41 | public: 42 | MenuItem() = default; 43 | explicit MenuItem(std::string title, std::function action): title(title), action(action) {} 44 | }; 45 | 46 | } // namespace margelo::nitro::nitrotext 47 | 48 | namespace margelo::nitro { 49 | 50 | // C++ MenuItem <> JS MenuItem (object) 51 | template <> 52 | struct JSIConverter final { 53 | static inline margelo::nitro::nitrotext::MenuItem fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 54 | jsi::Object obj = arg.asObject(runtime); 55 | return margelo::nitro::nitrotext::MenuItem( 56 | JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "title")), 57 | JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "action")) 58 | ); 59 | } 60 | static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitrotext::MenuItem& arg) { 61 | jsi::Object obj(runtime); 62 | obj.setProperty(runtime, "title", JSIConverter::toJSI(runtime, arg.title)); 63 | obj.setProperty(runtime, "action", JSIConverter>::toJSI(runtime, arg.action)); 64 | return obj; 65 | } 66 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 67 | if (!value.isObject()) { 68 | return false; 69 | } 70 | jsi::Object obj = value.getObject(runtime); 71 | if (!nitro::isPlainObject(runtime, obj)) { 72 | return false; 73 | } 74 | if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "title"))) return false; 75 | if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "action"))) return false; 76 | return true; 77 | } 78 | }; 79 | 80 | } // namespace margelo::nitro 81 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/Renderer.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// Renderer.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | namespace margelo::nitro::nitrotext { 27 | 28 | /** 29 | * An enum which can be represented as a JavaScript union (Renderer). 30 | */ 31 | enum class Renderer { 32 | HTML SWIFT_NAME(html) = 0, 33 | PLAINTEXT SWIFT_NAME(plaintext) = 1, 34 | } CLOSED_ENUM; 35 | 36 | } // namespace margelo::nitro::nitrotext 37 | 38 | namespace margelo::nitro { 39 | 40 | // C++ Renderer <> JS Renderer (union) 41 | template <> 42 | struct JSIConverter final { 43 | static inline margelo::nitro::nitrotext::Renderer fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 44 | std::string unionValue = JSIConverter::fromJSI(runtime, arg); 45 | switch (hashString(unionValue.c_str(), unionValue.size())) { 46 | case hashString("html"): return margelo::nitro::nitrotext::Renderer::HTML; 47 | case hashString("plaintext"): return margelo::nitro::nitrotext::Renderer::PLAINTEXT; 48 | default: [[unlikely]] 49 | throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum Renderer - invalid value!"); 50 | } 51 | } 52 | static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitrotext::Renderer arg) { 53 | switch (arg) { 54 | case margelo::nitro::nitrotext::Renderer::HTML: return JSIConverter::toJSI(runtime, "html"); 55 | case margelo::nitro::nitrotext::Renderer::PLAINTEXT: return JSIConverter::toJSI(runtime, "plaintext"); 56 | default: [[unlikely]] 57 | throw std::invalid_argument("Cannot convert Renderer to JS - invalid value: " 58 | + std::to_string(static_cast(arg)) + "!"); 59 | } 60 | } 61 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 62 | if (!value.isString()) { 63 | return false; 64 | } 65 | std::string unionValue = JSIConverter::fromJSI(runtime, value); 66 | switch (hashString(unionValue.c_str(), unionValue.size())) { 67 | case hashString("html"): 68 | case hashString("plaintext"): 69 | return true; 70 | default: 71 | return false; 72 | } 73 | } 74 | }; 75 | 76 | } // namespace margelo::nitro 77 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/swift/HybridNitroTextSpec.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// HybridNitroTextSpec.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | import Foundation 9 | import NitroModules 10 | 11 | /// See ``HybridNitroTextSpec`` 12 | public protocol HybridNitroTextSpec_protocol: HybridObject, HybridView { 13 | // Properties 14 | var fragments: [Fragment]? { get set } 15 | var renderer: Renderer? { get set } 16 | var selectable: Bool? { get set } 17 | var allowFontScaling: Bool? { get set } 18 | var ellipsizeMode: EllipsizeMode? { get set } 19 | var numberOfLines: Double? { get set } 20 | var lineBreakStrategyIOS: LineBreakStrategyIOS? { get set } 21 | var dynamicTypeRamp: DynamicTypeRamp? { get set } 22 | var maxFontSizeMultiplier: Double? { get set } 23 | var adjustsFontSizeToFit: Bool? { get set } 24 | var minimumFontScale: Double? { get set } 25 | var menus: [MenuItem]? { get set } 26 | var onTextLayout: ((_ layout: TextLayoutEvent) -> Void)? { get set } 27 | var onPress: (() -> Void)? { get set } 28 | var onPressIn: (() -> Void)? { get set } 29 | var onPressOut: (() -> Void)? { get set } 30 | var text: String? { get set } 31 | var selectionColor: String? { get set } 32 | var fontSize: Double? { get set } 33 | var fontWeight: FontWeight? { get set } 34 | var fontColor: String? { get set } 35 | var fragmentBackgroundColor: String? { get set } 36 | var fontStyle: FontStyle? { get set } 37 | var fontFamily: String? { get set } 38 | var lineHeight: Double? { get set } 39 | var letterSpacing: Double? { get set } 40 | var textAlign: TextAlign? { get set } 41 | var textTransform: TextTransform? { get set } 42 | var textDecorationLine: TextDecorationLine? { get set } 43 | var textDecorationColor: String? { get set } 44 | var textDecorationStyle: TextDecorationStyle? { get set } 45 | 46 | // Methods 47 | 48 | } 49 | 50 | public extension HybridNitroTextSpec_protocol { 51 | /// Default implementation of ``HybridObject.toString`` 52 | func toString() -> String { 53 | return "[HybridObject NitroText]" 54 | } 55 | } 56 | 57 | /// See ``HybridNitroTextSpec`` 58 | open class HybridNitroTextSpec_base { 59 | private weak var cxxWrapper: HybridNitroTextSpec_cxx? = nil 60 | public init() { } 61 | public func getCxxWrapper() -> HybridNitroTextSpec_cxx { 62 | #if DEBUG 63 | guard self is HybridNitroTextSpec else { 64 | fatalError("`self` is not a `HybridNitroTextSpec`! Did you accidentally inherit from `HybridNitroTextSpec_base` instead of `HybridNitroTextSpec`?") 65 | } 66 | #endif 67 | if let cxxWrapper = self.cxxWrapper { 68 | return cxxWrapper 69 | } else { 70 | let cxxWrapper = HybridNitroTextSpec_cxx(self as! HybridNitroTextSpec) 71 | self.cxxWrapper = cxxWrapper 72 | return cxxWrapper 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * A Swift base-protocol representing the NitroText HybridObject. 79 | * Implement this protocol to create Swift-based instances of NitroText. 80 | * ```swift 81 | * class HybridNitroText : HybridNitroTextSpec { 82 | * // ... 83 | * } 84 | * ``` 85 | */ 86 | public typealias HybridNitroTextSpec = HybridNitroTextSpec_protocol & HybridNitroTextSpec_base 87 | -------------------------------------------------------------------------------- /.github/workflows/ios-build.yml: -------------------------------------------------------------------------------- 1 | name: Build iOS 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - '.github/workflows/ios-build.yml' 12 | - 'example/ios/**' 13 | - 'example/Gemfile' 14 | - 'example/Gemfile.lock' 15 | - '**/nitrogen/generated/shared/**' 16 | - '**/nitrogen/generated/ios/**' 17 | - 'cpp/**' 18 | - 'ios/**' 19 | - '**/Podfile.lock' 20 | - '**/bun.lock' 21 | - '**/*.podspec' 22 | - '**/react-native.config.js' 23 | - '**/nitro.json' 24 | pull_request: 25 | paths: 26 | - '.github/workflows/ios-build.yml' 27 | - 'example/ios/**' 28 | - 'example/Gemfile' 29 | - 'example/Gemfile.lock' 30 | - '**/nitrogen/generated/shared/**' 31 | - '**/nitrogen/generated/ios/**' 32 | - 'cpp/**' 33 | - 'ios/**' 34 | - '**/Podfile.lock' 35 | - '**/bun.lock' 36 | - '**/*.podspec' 37 | - '**/react-native.config.js' 38 | - '**/nitro.json' 39 | workflow_dispatch: 40 | 41 | env: 42 | USE_CCACHE: 1 43 | 44 | concurrency: 45 | group: ${{ github.workflow }}-${{ github.ref }} 46 | cancel-in-progress: true 47 | 48 | jobs: 49 | build: 50 | name: Build iOS Example App (${{ matrix.arch }}) 51 | runs-on: macOS-15 52 | strategy: 53 | fail-fast: false 54 | matrix: 55 | arch: [new, old] 56 | steps: 57 | - uses: actions/checkout@v6 58 | - uses: oven-sh/setup-bun@v2 59 | - name: Setup Xcode 60 | uses: maxim-lobanov/setup-xcode@v1 61 | with: 62 | xcode-version: 16.4 63 | 64 | - name: Install dependencies (bun) 65 | run: bun install 66 | 67 | - name: Disable new architecture in Podfile 68 | if: matrix.arch == 'old' 69 | run: sed -i "" "s/ENV\['RCT_NEW_ARCH_ENABLED'\] = '1'/ENV['RCT_NEW_ARCH_ENABLED'] = '0'/g" example/ios/Podfile 70 | 71 | - name: Setup Ruby (bundle) 72 | uses: ruby/setup-ruby@v1 73 | with: 74 | ruby-version: '3.2' 75 | bundler-cache: true 76 | working-directory: example/ios 77 | 78 | - name: Install xcpretty 79 | run: gem install xcpretty 80 | 81 | # - name: Cache CocoaPods 82 | # uses: actions/cache@v4 83 | # with: 84 | # path: example/ios/Pods 85 | # key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile') }} 86 | # restore-keys: | 87 | # ${{ runner.os }}-pods- 88 | 89 | - name: Install Pods 90 | working-directory: example/ios 91 | run: pod install 92 | 93 | - name: Build App 94 | working-directory: example/ios 95 | run: | 96 | set -o pipefail && xcodebuild \ 97 | CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ \ 98 | -derivedDataPath build -UseModernBuildSystem=YES \ 99 | -workspace NitroTextExample.xcworkspace \ 100 | -scheme NitroTextExample \ 101 | -sdk iphonesimulator \ 102 | -configuration Debug \ 103 | -destination 'platform=iOS Simulator,name=iPhone 16' \ 104 | build \ 105 | CODE_SIGNING_ALLOWED=NO | xcpretty 106 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/FontStyle.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// FontStyle.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | namespace margelo::nitro::nitrotext { 27 | 28 | /** 29 | * An enum which can be represented as a JavaScript union (FontStyle). 30 | */ 31 | enum class FontStyle { 32 | NORMAL SWIFT_NAME(normal) = 0, 33 | ITALIC SWIFT_NAME(italic) = 1, 34 | OBLIQUE SWIFT_NAME(oblique) = 2, 35 | } CLOSED_ENUM; 36 | 37 | } // namespace margelo::nitro::nitrotext 38 | 39 | namespace margelo::nitro { 40 | 41 | // C++ FontStyle <> JS FontStyle (union) 42 | template <> 43 | struct JSIConverter final { 44 | static inline margelo::nitro::nitrotext::FontStyle fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 45 | std::string unionValue = JSIConverter::fromJSI(runtime, arg); 46 | switch (hashString(unionValue.c_str(), unionValue.size())) { 47 | case hashString("normal"): return margelo::nitro::nitrotext::FontStyle::NORMAL; 48 | case hashString("italic"): return margelo::nitro::nitrotext::FontStyle::ITALIC; 49 | case hashString("oblique"): return margelo::nitro::nitrotext::FontStyle::OBLIQUE; 50 | default: [[unlikely]] 51 | throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum FontStyle - invalid value!"); 52 | } 53 | } 54 | static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitrotext::FontStyle arg) { 55 | switch (arg) { 56 | case margelo::nitro::nitrotext::FontStyle::NORMAL: return JSIConverter::toJSI(runtime, "normal"); 57 | case margelo::nitro::nitrotext::FontStyle::ITALIC: return JSIConverter::toJSI(runtime, "italic"); 58 | case margelo::nitro::nitrotext::FontStyle::OBLIQUE: return JSIConverter::toJSI(runtime, "oblique"); 59 | default: [[unlikely]] 60 | throw std::invalid_argument("Cannot convert FontStyle to JS - invalid value: " 61 | + std::to_string(static_cast(arg)) + "!"); 62 | } 63 | } 64 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 65 | if (!value.isString()) { 66 | return false; 67 | } 68 | std::string unionValue = JSIConverter::fromJSI(runtime, value); 69 | switch (hashString(unionValue.c_str(), unionValue.size())) { 70 | case hashString("normal"): 71 | case hashString("italic"): 72 | case hashString("oblique"): 73 | return true; 74 | default: 75 | return false; 76 | } 77 | } 78 | }; 79 | 80 | } // namespace margelo::nitro 81 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.2.3) 9 | base64 10 | benchmark (>= 0.3) 11 | bigdecimal 12 | concurrent-ruby (~> 1.0, >= 1.3.1) 13 | connection_pool (>= 2.2.5) 14 | drb 15 | i18n (>= 1.6, < 2) 16 | logger (>= 1.4.2) 17 | minitest (>= 5.1) 18 | securerandom (>= 0.3) 19 | tzinfo (~> 2.0, >= 2.0.5) 20 | addressable (2.8.7) 21 | public_suffix (>= 2.0.2, < 7.0) 22 | algoliasearch (1.27.5) 23 | httpclient (~> 2.8, >= 2.8.3) 24 | json (>= 1.5.1) 25 | atomos (0.1.3) 26 | base64 (0.3.0) 27 | benchmark (0.5.0) 28 | bigdecimal (3.3.1) 29 | claide (1.1.0) 30 | cocoapods (1.16.2) 31 | addressable (~> 2.8) 32 | claide (>= 1.0.2, < 2.0) 33 | cocoapods-core (= 1.16.2) 34 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 35 | cocoapods-downloader (>= 2.1, < 3.0) 36 | cocoapods-plugins (>= 1.0.0, < 2.0) 37 | cocoapods-search (>= 1.0.0, < 2.0) 38 | cocoapods-trunk (>= 1.6.0, < 2.0) 39 | cocoapods-try (>= 1.1.0, < 2.0) 40 | colored2 (~> 3.1) 41 | escape (~> 0.0.4) 42 | fourflusher (>= 2.3.0, < 3.0) 43 | gh_inspector (~> 1.0) 44 | molinillo (~> 0.8.0) 45 | nap (~> 1.0) 46 | ruby-macho (>= 2.3.0, < 3.0) 47 | xcodeproj (>= 1.27.0, < 2.0) 48 | cocoapods-core (1.16.2) 49 | activesupport (>= 5.0, < 8) 50 | addressable (~> 2.8) 51 | algoliasearch (~> 1.0) 52 | concurrent-ruby (~> 1.1) 53 | fuzzy_match (~> 2.0.4) 54 | nap (~> 1.0) 55 | netrc (~> 0.11) 56 | public_suffix (~> 4.0) 57 | typhoeus (~> 1.0) 58 | cocoapods-deintegrate (1.0.5) 59 | cocoapods-downloader (2.1) 60 | cocoapods-plugins (1.0.0) 61 | nap 62 | cocoapods-search (1.0.1) 63 | cocoapods-trunk (1.6.0) 64 | nap (>= 0.8, < 2.0) 65 | netrc (~> 0.11) 66 | cocoapods-try (1.2.0) 67 | colored2 (3.1.2) 68 | concurrent-ruby (1.3.5) 69 | connection_pool (2.5.4) 70 | drb (2.2.3) 71 | escape (0.0.4) 72 | ethon (0.15.0) 73 | ffi (>= 1.15.0) 74 | ffi (1.17.2) 75 | fourflusher (2.3.1) 76 | fuzzy_match (2.0.4) 77 | gh_inspector (1.1.3) 78 | httpclient (2.9.0) 79 | mutex_m 80 | i18n (1.14.7) 81 | concurrent-ruby (~> 1.0) 82 | json (2.13.2) 83 | logger (1.7.0) 84 | minitest (5.26.0) 85 | molinillo (0.8.0) 86 | mutex_m (0.3.0) 87 | nanaimo (0.4.0) 88 | nap (1.1.0) 89 | netrc (0.11.0) 90 | nkf (0.2.0) 91 | public_suffix (4.0.7) 92 | rexml (3.4.4) 93 | ruby-macho (2.5.1) 94 | securerandom (0.4.1) 95 | typhoeus (1.5.0) 96 | ethon (>= 0.9.0, < 0.16.0) 97 | tzinfo (2.0.6) 98 | concurrent-ruby (~> 1.0) 99 | xcodeproj (1.27.0) 100 | CFPropertyList (>= 2.3.3, < 4.0) 101 | atomos (~> 0.1.3) 102 | claide (>= 1.0.2, < 2.0) 103 | colored2 (~> 3.1) 104 | nanaimo (~> 0.4.0) 105 | rexml (>= 3.3.6, < 4.0) 106 | 107 | PLATFORMS 108 | ruby 109 | 110 | DEPENDENCIES 111 | activesupport (>= 6.1.7.5, != 7.1.0) 112 | benchmark 113 | bigdecimal 114 | cocoapods (>= 1.13, != 1.15.1, != 1.15.0) 115 | concurrent-ruby (< 1.3.6) 116 | logger 117 | mutex_m 118 | xcodeproj (< 1.28.0) 119 | 120 | RUBY VERSION 121 | ruby 3.3.6p108 122 | 123 | BUNDLED WITH 124 | 2.5.22 125 | -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @REM Copyright (c) Meta Platforms, Inc. and affiliates. 2 | @REM 3 | @REM This source code is licensed under the MIT license found in the 4 | @REM LICENSE file in the root directory of this source tree. 5 | 6 | @rem 7 | @rem Copyright 2015 the original author or authors. 8 | @rem 9 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 10 | @rem you may not use this file except in compliance with the License. 11 | @rem You may obtain a copy of the License at 12 | @rem 13 | @rem https://www.apache.org/licenses/LICENSE-2.0 14 | @rem 15 | @rem Unless required by applicable law or agreed to in writing, software 16 | @rem distributed under the License is distributed on an "AS IS" BASIS, 17 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | @rem See the License for the specific language governing permissions and 19 | @rem limitations under the License. 20 | @rem 21 | @rem SPDX-License-Identifier: Apache-2.0 22 | @rem 23 | 24 | @if "%DEBUG%"=="" @echo off 25 | @rem ########################################################################## 26 | @rem 27 | @rem Gradle startup script for Windows 28 | @rem 29 | @rem ########################################################################## 30 | 31 | @rem Set local scope for the variables with windows NT shell 32 | if "%OS%"=="Windows_NT" setlocal 33 | 34 | set DIRNAME=%~dp0 35 | if "%DIRNAME%"=="" set DIRNAME=. 36 | @rem This is normally unused 37 | set APP_BASE_NAME=%~n0 38 | set APP_HOME=%DIRNAME% 39 | 40 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 41 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 42 | 43 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 44 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 45 | 46 | @rem Find java.exe 47 | if defined JAVA_HOME goto findJavaFromJavaHome 48 | 49 | set JAVA_EXE=java.exe 50 | %JAVA_EXE% -version >NUL 2>&1 51 | if %ERRORLEVEL% equ 0 goto execute 52 | 53 | echo. 1>&2 54 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 55 | echo. 1>&2 56 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 57 | echo location of your Java installation. 1>&2 58 | 59 | goto fail 60 | 61 | :findJavaFromJavaHome 62 | set JAVA_HOME=%JAVA_HOME:"=% 63 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 64 | 65 | if exist "%JAVA_EXE%" goto execute 66 | 67 | echo. 1>&2 68 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 69 | echo. 1>&2 70 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 71 | echo location of your Java installation. 1>&2 72 | 73 | goto fail 74 | 75 | :execute 76 | @rem Setup the command line 77 | 78 | set CLASSPATH= 79 | 80 | 81 | @rem Execute Gradle 82 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 83 | 84 | :end 85 | @rem End local scope for the variables with windows NT shell 86 | if %ERRORLEVEL% equ 0 goto mainEnd 87 | 88 | :fail 89 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 90 | rem the _cmd.exe /c_ return code! 91 | set EXIT_CODE=%ERRORLEVEL% 92 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 93 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 94 | exit /b %EXIT_CODE% 95 | 96 | :mainEnd 97 | if "%OS%"=="Windows_NT" endlocal 98 | 99 | :omega 100 | -------------------------------------------------------------------------------- /example/ios/NitroTextExample.xcodeproj/xcshareddata/xcschemes/NitroTextExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/EllipsizeMode.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// EllipsizeMode.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | namespace margelo::nitro::nitrotext { 27 | 28 | /** 29 | * An enum which can be represented as a JavaScript union (EllipsizeMode). 30 | */ 31 | enum class EllipsizeMode { 32 | HEAD SWIFT_NAME(head) = 0, 33 | MIDDLE SWIFT_NAME(middle) = 1, 34 | TAIL SWIFT_NAME(tail) = 2, 35 | CLIP SWIFT_NAME(clip) = 3, 36 | } CLOSED_ENUM; 37 | 38 | } // namespace margelo::nitro::nitrotext 39 | 40 | namespace margelo::nitro { 41 | 42 | // C++ EllipsizeMode <> JS EllipsizeMode (union) 43 | template <> 44 | struct JSIConverter final { 45 | static inline margelo::nitro::nitrotext::EllipsizeMode fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 46 | std::string unionValue = JSIConverter::fromJSI(runtime, arg); 47 | switch (hashString(unionValue.c_str(), unionValue.size())) { 48 | case hashString("head"): return margelo::nitro::nitrotext::EllipsizeMode::HEAD; 49 | case hashString("middle"): return margelo::nitro::nitrotext::EllipsizeMode::MIDDLE; 50 | case hashString("tail"): return margelo::nitro::nitrotext::EllipsizeMode::TAIL; 51 | case hashString("clip"): return margelo::nitro::nitrotext::EllipsizeMode::CLIP; 52 | default: [[unlikely]] 53 | throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum EllipsizeMode - invalid value!"); 54 | } 55 | } 56 | static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitrotext::EllipsizeMode arg) { 57 | switch (arg) { 58 | case margelo::nitro::nitrotext::EllipsizeMode::HEAD: return JSIConverter::toJSI(runtime, "head"); 59 | case margelo::nitro::nitrotext::EllipsizeMode::MIDDLE: return JSIConverter::toJSI(runtime, "middle"); 60 | case margelo::nitro::nitrotext::EllipsizeMode::TAIL: return JSIConverter::toJSI(runtime, "tail"); 61 | case margelo::nitro::nitrotext::EllipsizeMode::CLIP: return JSIConverter::toJSI(runtime, "clip"); 62 | default: [[unlikely]] 63 | throw std::invalid_argument("Cannot convert EllipsizeMode to JS - invalid value: " 64 | + std::to_string(static_cast(arg)) + "!"); 65 | } 66 | } 67 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 68 | if (!value.isString()) { 69 | return false; 70 | } 71 | std::string unionValue = JSIConverter::fromJSI(runtime, value); 72 | switch (hashString(unionValue.c_str(), unionValue.size())) { 73 | case hashString("head"): 74 | case hashString("middle"): 75 | case hashString("tail"): 76 | case hashString("clip"): 77 | return true; 78 | default: 79 | return false; 80 | } 81 | } 82 | }; 83 | 84 | } // namespace margelo::nitro 85 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/TextTransform.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextTransform.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | namespace margelo::nitro::nitrotext { 27 | 28 | /** 29 | * An enum which can be represented as a JavaScript union (TextTransform). 30 | */ 31 | enum class TextTransform { 32 | NONE SWIFT_NAME(none) = 0, 33 | UPPERCASE SWIFT_NAME(uppercase) = 1, 34 | LOWERCASE SWIFT_NAME(lowercase) = 2, 35 | CAPITALIZE SWIFT_NAME(capitalize) = 3, 36 | } CLOSED_ENUM; 37 | 38 | } // namespace margelo::nitro::nitrotext 39 | 40 | namespace margelo::nitro { 41 | 42 | // C++ TextTransform <> JS TextTransform (union) 43 | template <> 44 | struct JSIConverter final { 45 | static inline margelo::nitro::nitrotext::TextTransform fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 46 | std::string unionValue = JSIConverter::fromJSI(runtime, arg); 47 | switch (hashString(unionValue.c_str(), unionValue.size())) { 48 | case hashString("none"): return margelo::nitro::nitrotext::TextTransform::NONE; 49 | case hashString("uppercase"): return margelo::nitro::nitrotext::TextTransform::UPPERCASE; 50 | case hashString("lowercase"): return margelo::nitro::nitrotext::TextTransform::LOWERCASE; 51 | case hashString("capitalize"): return margelo::nitro::nitrotext::TextTransform::CAPITALIZE; 52 | default: [[unlikely]] 53 | throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum TextTransform - invalid value!"); 54 | } 55 | } 56 | static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitrotext::TextTransform arg) { 57 | switch (arg) { 58 | case margelo::nitro::nitrotext::TextTransform::NONE: return JSIConverter::toJSI(runtime, "none"); 59 | case margelo::nitro::nitrotext::TextTransform::UPPERCASE: return JSIConverter::toJSI(runtime, "uppercase"); 60 | case margelo::nitro::nitrotext::TextTransform::LOWERCASE: return JSIConverter::toJSI(runtime, "lowercase"); 61 | case margelo::nitro::nitrotext::TextTransform::CAPITALIZE: return JSIConverter::toJSI(runtime, "capitalize"); 62 | default: [[unlikely]] 63 | throw std::invalid_argument("Cannot convert TextTransform to JS - invalid value: " 64 | + std::to_string(static_cast(arg)) + "!"); 65 | } 66 | } 67 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 68 | if (!value.isString()) { 69 | return false; 70 | } 71 | std::string unionValue = JSIConverter::fromJSI(runtime, value); 72 | switch (hashString(unionValue.c_str(), unionValue.size())) { 73 | case hashString("none"): 74 | case hashString("uppercase"): 75 | case hashString("lowercase"): 76 | case hashString("capitalize"): 77 | return true; 78 | default: 79 | return false; 80 | } 81 | } 82 | }; 83 | 84 | } // namespace margelo::nitro 85 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/TextDecorationStyle.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextDecorationStyle.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | namespace margelo::nitro::nitrotext { 27 | 28 | /** 29 | * An enum which can be represented as a JavaScript union (TextDecorationStyle). 30 | */ 31 | enum class TextDecorationStyle { 32 | SOLID SWIFT_NAME(solid) = 0, 33 | DOUBLE SWIFT_NAME(double) = 1, 34 | DOTTED SWIFT_NAME(dotted) = 2, 35 | DASHED SWIFT_NAME(dashed) = 3, 36 | } CLOSED_ENUM; 37 | 38 | } // namespace margelo::nitro::nitrotext 39 | 40 | namespace margelo::nitro { 41 | 42 | // C++ TextDecorationStyle <> JS TextDecorationStyle (union) 43 | template <> 44 | struct JSIConverter final { 45 | static inline margelo::nitro::nitrotext::TextDecorationStyle fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 46 | std::string unionValue = JSIConverter::fromJSI(runtime, arg); 47 | switch (hashString(unionValue.c_str(), unionValue.size())) { 48 | case hashString("solid"): return margelo::nitro::nitrotext::TextDecorationStyle::SOLID; 49 | case hashString("double"): return margelo::nitro::nitrotext::TextDecorationStyle::DOUBLE; 50 | case hashString("dotted"): return margelo::nitro::nitrotext::TextDecorationStyle::DOTTED; 51 | case hashString("dashed"): return margelo::nitro::nitrotext::TextDecorationStyle::DASHED; 52 | default: [[unlikely]] 53 | throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum TextDecorationStyle - invalid value!"); 54 | } 55 | } 56 | static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitrotext::TextDecorationStyle arg) { 57 | switch (arg) { 58 | case margelo::nitro::nitrotext::TextDecorationStyle::SOLID: return JSIConverter::toJSI(runtime, "solid"); 59 | case margelo::nitro::nitrotext::TextDecorationStyle::DOUBLE: return JSIConverter::toJSI(runtime, "double"); 60 | case margelo::nitro::nitrotext::TextDecorationStyle::DOTTED: return JSIConverter::toJSI(runtime, "dotted"); 61 | case margelo::nitro::nitrotext::TextDecorationStyle::DASHED: return JSIConverter::toJSI(runtime, "dashed"); 62 | default: [[unlikely]] 63 | throw std::invalid_argument("Cannot convert TextDecorationStyle to JS - invalid value: " 64 | + std::to_string(static_cast(arg)) + "!"); 65 | } 66 | } 67 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 68 | if (!value.isString()) { 69 | return false; 70 | } 71 | std::string unionValue = JSIConverter::fromJSI(runtime, value); 72 | switch (hashString(unionValue.c_str(), unionValue.size())) { 73 | case hashString("solid"): 74 | case hashString("double"): 75 | case hashString("dotted"): 76 | case hashString("dashed"): 77 | return true; 78 | default: 79 | return false; 80 | } 81 | } 82 | }; 83 | 84 | } // namespace margelo::nitro 85 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). 2 | 3 | # Getting Started 4 | 5 | > **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding. 6 | 7 | ## Step 1: Start Metro 8 | 9 | First, you will need to run **Metro**, the JavaScript build tool for React Native. 10 | 11 | To start the Metro dev server, run the following command from the root of your React Native project: 12 | 13 | ```sh 14 | # Using npm 15 | npm start 16 | 17 | # OR using Yarn 18 | yarn start 19 | ``` 20 | 21 | ## Step 2: Build and run your app 22 | 23 | With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app: 24 | 25 | ### Android 26 | 27 | ```sh 28 | # Using npm 29 | npm run android 30 | 31 | # OR using Yarn 32 | yarn android 33 | ``` 34 | 35 | ### iOS 36 | 37 | For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps). 38 | 39 | The first time you create a new project, run the Ruby bundler to install CocoaPods itself: 40 | 41 | ```sh 42 | bundle install 43 | ``` 44 | 45 | Then, and every time you update your native dependencies, run: 46 | 47 | ```sh 48 | bundle exec pod install 49 | ``` 50 | 51 | For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html). 52 | 53 | ```sh 54 | # Using npm 55 | npm run ios 56 | 57 | # OR using Yarn 58 | yarn ios 59 | ``` 60 | 61 | If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device. 62 | 63 | This is one way to run your app — you can also build it directly from Android Studio or Xcode. 64 | 65 | ## Step 3: Modify your app 66 | 67 | Now that you have successfully run the app, let's make changes! 68 | 69 | Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). 70 | 71 | When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: 72 | 73 | - **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS). 74 | - **iOS**: Press R in iOS Simulator. 75 | 76 | ## Congratulations! :tada: 77 | 78 | You've successfully run and modified your React Native App. :partying_face: 79 | 80 | ### Now what? 81 | 82 | - If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). 83 | - If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started). 84 | 85 | # Troubleshooting 86 | 87 | If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. 88 | 89 | # Learn More 90 | 91 | To learn more about React Native, take a look at the following resources: 92 | 93 | - [React Native Website](https://reactnative.dev) - learn more about React Native. 94 | - [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. 95 | - [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. 96 | - [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. 97 | - [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. 98 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/LineBreakStrategyIOS.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// LineBreakStrategyIOS.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | namespace margelo::nitro::nitrotext { 27 | 28 | /** 29 | * An enum which can be represented as a JavaScript union (LineBreakStrategyIOS). 30 | */ 31 | enum class LineBreakStrategyIOS { 32 | NONE SWIFT_NAME(none) = 0, 33 | STANDARD SWIFT_NAME(standard) = 1, 34 | HANGUL_WORD SWIFT_NAME(hangulWord) = 2, 35 | PUSH_OUT SWIFT_NAME(pushOut) = 3, 36 | } CLOSED_ENUM; 37 | 38 | } // namespace margelo::nitro::nitrotext 39 | 40 | namespace margelo::nitro { 41 | 42 | // C++ LineBreakStrategyIOS <> JS LineBreakStrategyIOS (union) 43 | template <> 44 | struct JSIConverter final { 45 | static inline margelo::nitro::nitrotext::LineBreakStrategyIOS fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 46 | std::string unionValue = JSIConverter::fromJSI(runtime, arg); 47 | switch (hashString(unionValue.c_str(), unionValue.size())) { 48 | case hashString("none"): return margelo::nitro::nitrotext::LineBreakStrategyIOS::NONE; 49 | case hashString("standard"): return margelo::nitro::nitrotext::LineBreakStrategyIOS::STANDARD; 50 | case hashString("hangul-word"): return margelo::nitro::nitrotext::LineBreakStrategyIOS::HANGUL_WORD; 51 | case hashString("push-out"): return margelo::nitro::nitrotext::LineBreakStrategyIOS::PUSH_OUT; 52 | default: [[unlikely]] 53 | throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum LineBreakStrategyIOS - invalid value!"); 54 | } 55 | } 56 | static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitrotext::LineBreakStrategyIOS arg) { 57 | switch (arg) { 58 | case margelo::nitro::nitrotext::LineBreakStrategyIOS::NONE: return JSIConverter::toJSI(runtime, "none"); 59 | case margelo::nitro::nitrotext::LineBreakStrategyIOS::STANDARD: return JSIConverter::toJSI(runtime, "standard"); 60 | case margelo::nitro::nitrotext::LineBreakStrategyIOS::HANGUL_WORD: return JSIConverter::toJSI(runtime, "hangul-word"); 61 | case margelo::nitro::nitrotext::LineBreakStrategyIOS::PUSH_OUT: return JSIConverter::toJSI(runtime, "push-out"); 62 | default: [[unlikely]] 63 | throw std::invalid_argument("Cannot convert LineBreakStrategyIOS to JS - invalid value: " 64 | + std::to_string(static_cast(arg)) + "!"); 65 | } 66 | } 67 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 68 | if (!value.isString()) { 69 | return false; 70 | } 71 | std::string unionValue = JSIConverter::fromJSI(runtime, value); 72 | switch (hashString(unionValue.c_str(), unionValue.size())) { 73 | case hashString("none"): 74 | case hashString("standard"): 75 | case hashString("hangul-word"): 76 | case hashString("push-out"): 77 | return true; 78 | default: 79 | return false; 80 | } 81 | } 82 | }; 83 | 84 | } // namespace margelo::nitro 85 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/TextAlign.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextAlign.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | namespace margelo::nitro::nitrotext { 27 | 28 | /** 29 | * An enum which can be represented as a JavaScript union (TextAlign). 30 | */ 31 | enum class TextAlign { 32 | AUTO SWIFT_NAME(auto) = 0, 33 | LEFT SWIFT_NAME(left) = 1, 34 | RIGHT SWIFT_NAME(right) = 2, 35 | CENTER SWIFT_NAME(center) = 3, 36 | JUSTIFY SWIFT_NAME(justify) = 4, 37 | } CLOSED_ENUM; 38 | 39 | } // namespace margelo::nitro::nitrotext 40 | 41 | namespace margelo::nitro { 42 | 43 | // C++ TextAlign <> JS TextAlign (union) 44 | template <> 45 | struct JSIConverter final { 46 | static inline margelo::nitro::nitrotext::TextAlign fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 47 | std::string unionValue = JSIConverter::fromJSI(runtime, arg); 48 | switch (hashString(unionValue.c_str(), unionValue.size())) { 49 | case hashString("auto"): return margelo::nitro::nitrotext::TextAlign::AUTO; 50 | case hashString("left"): return margelo::nitro::nitrotext::TextAlign::LEFT; 51 | case hashString("right"): return margelo::nitro::nitrotext::TextAlign::RIGHT; 52 | case hashString("center"): return margelo::nitro::nitrotext::TextAlign::CENTER; 53 | case hashString("justify"): return margelo::nitro::nitrotext::TextAlign::JUSTIFY; 54 | default: [[unlikely]] 55 | throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum TextAlign - invalid value!"); 56 | } 57 | } 58 | static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitrotext::TextAlign arg) { 59 | switch (arg) { 60 | case margelo::nitro::nitrotext::TextAlign::AUTO: return JSIConverter::toJSI(runtime, "auto"); 61 | case margelo::nitro::nitrotext::TextAlign::LEFT: return JSIConverter::toJSI(runtime, "left"); 62 | case margelo::nitro::nitrotext::TextAlign::RIGHT: return JSIConverter::toJSI(runtime, "right"); 63 | case margelo::nitro::nitrotext::TextAlign::CENTER: return JSIConverter::toJSI(runtime, "center"); 64 | case margelo::nitro::nitrotext::TextAlign::JUSTIFY: return JSIConverter::toJSI(runtime, "justify"); 65 | default: [[unlikely]] 66 | throw std::invalid_argument("Cannot convert TextAlign to JS - invalid value: " 67 | + std::to_string(static_cast(arg)) + "!"); 68 | } 69 | } 70 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 71 | if (!value.isString()) { 72 | return false; 73 | } 74 | std::string unionValue = JSIConverter::fromJSI(runtime, value); 75 | switch (hashString(unionValue.c_str(), unionValue.size())) { 76 | case hashString("auto"): 77 | case hashString("left"): 78 | case hashString("right"): 79 | case hashString("center"): 80 | case hashString("justify"): 81 | return true; 82 | default: 83 | return false; 84 | } 85 | } 86 | }; 87 | 88 | } // namespace margelo::nitro 89 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/TextDecorationLine.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextDecorationLine.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | namespace margelo::nitro::nitrotext { 27 | 28 | /** 29 | * An enum which can be represented as a JavaScript union (TextDecorationLine). 30 | */ 31 | enum class TextDecorationLine { 32 | NONE SWIFT_NAME(none) = 0, 33 | UNDERLINE SWIFT_NAME(underline) = 1, 34 | LINE_THROUGH SWIFT_NAME(lineThrough) = 2, 35 | UNDERLINE_LINE_THROUGH SWIFT_NAME(underlineLineThrough) = 3, 36 | } CLOSED_ENUM; 37 | 38 | } // namespace margelo::nitro::nitrotext 39 | 40 | namespace margelo::nitro { 41 | 42 | // C++ TextDecorationLine <> JS TextDecorationLine (union) 43 | template <> 44 | struct JSIConverter final { 45 | static inline margelo::nitro::nitrotext::TextDecorationLine fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 46 | std::string unionValue = JSIConverter::fromJSI(runtime, arg); 47 | switch (hashString(unionValue.c_str(), unionValue.size())) { 48 | case hashString("none"): return margelo::nitro::nitrotext::TextDecorationLine::NONE; 49 | case hashString("underline"): return margelo::nitro::nitrotext::TextDecorationLine::UNDERLINE; 50 | case hashString("line-through"): return margelo::nitro::nitrotext::TextDecorationLine::LINE_THROUGH; 51 | case hashString("underline line-through"): return margelo::nitro::nitrotext::TextDecorationLine::UNDERLINE_LINE_THROUGH; 52 | default: [[unlikely]] 53 | throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum TextDecorationLine - invalid value!"); 54 | } 55 | } 56 | static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitrotext::TextDecorationLine arg) { 57 | switch (arg) { 58 | case margelo::nitro::nitrotext::TextDecorationLine::NONE: return JSIConverter::toJSI(runtime, "none"); 59 | case margelo::nitro::nitrotext::TextDecorationLine::UNDERLINE: return JSIConverter::toJSI(runtime, "underline"); 60 | case margelo::nitro::nitrotext::TextDecorationLine::LINE_THROUGH: return JSIConverter::toJSI(runtime, "line-through"); 61 | case margelo::nitro::nitrotext::TextDecorationLine::UNDERLINE_LINE_THROUGH: return JSIConverter::toJSI(runtime, "underline line-through"); 62 | default: [[unlikely]] 63 | throw std::invalid_argument("Cannot convert TextDecorationLine to JS - invalid value: " 64 | + std::to_string(static_cast(arg)) + "!"); 65 | } 66 | } 67 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 68 | if (!value.isString()) { 69 | return false; 70 | } 71 | std::string unionValue = JSIConverter::fromJSI(runtime, value); 72 | switch (hashString(unionValue.c_str(), unionValue.size())) { 73 | case hashString("none"): 74 | case hashString("underline"): 75 | case hashString("line-through"): 76 | case hashString("underline line-through"): 77 | return true; 78 | default: 79 | return false; 80 | } 81 | } 82 | }; 83 | 84 | } // namespace margelo::nitro 85 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | type TextAlign = 'auto' | 'left' | 'right' | 'center' | 'justify' 2 | type TextTransform = 'none' | 'uppercase' | 'lowercase' | 'capitalize' 3 | export type EllipsizeMode = 'head' | 'middle' | 'tail' | 'clip' 4 | export type LineBreakStrategyIOS = 5 | | 'none' 6 | | 'standard' 7 | | 'hangul-word' 8 | | 'push-out' 9 | export type DynamicTypeRamp = 10 | | 'caption2' 11 | | 'caption1' 12 | | 'footnote' 13 | | 'subheadline' 14 | | 'callout' 15 | | 'body' 16 | | 'headline' 17 | | 'title3' 18 | | 'title2' 19 | | 'title1' 20 | | 'largeTitle' 21 | 22 | // '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' - Nitro does not support these 23 | type FontWeight = 24 | | 'normal' 25 | | 'bold' 26 | | 'ultralight' 27 | | 'thin' 28 | | 'light' 29 | | 'medium' 30 | | 'regular' 31 | | 'semibold' 32 | | 'condensedBold' 33 | | 'condensed' 34 | | 'heavy' 35 | | 'black' 36 | type FontStyle = 'normal' | 'italic' | 'oblique' 37 | 38 | type TextDecorationLine = 39 | | 'none' 40 | | 'underline' 41 | | 'line-through' 42 | | 'underline line-through' 43 | type TextDecorationStyle = 'solid' | 'double' | 'dotted' | 'dashed' 44 | 45 | export type TextLayout = { 46 | text: string 47 | x: number 48 | y: number 49 | width: number 50 | height: number 51 | descender: number 52 | capHeight: number 53 | ascender: number 54 | xHeight: number 55 | } 56 | 57 | export type TextLayoutEvent = { 58 | lines: Array 59 | } 60 | 61 | export type Fragment = { 62 | /** 63 | * The text of the text. 64 | */ 65 | text?: string 66 | 67 | /** 68 | * iOS: Color for text selection highlight/caret. 69 | */ 70 | selectionColor?: string 71 | 72 | /** 73 | * The font size of the text. 74 | */ 75 | fontSize?: number 76 | 77 | /** 78 | * The font weight of the text. 79 | */ 80 | fontWeight?: FontWeight 81 | 82 | /** 83 | * The font color of the text. 84 | */ 85 | fontColor?: string 86 | 87 | /** 88 | * Background highlight behind this text fragment. 89 | * Mirrors React Native Text's `backgroundColor` when applied to nested runs. 90 | * Named differently to avoid clashing with view style `backgroundColor`. 91 | */ 92 | fragmentBackgroundColor?: string 93 | 94 | /** 95 | * The font style of the text (italic, normal). 96 | */ 97 | fontStyle?: FontStyle 98 | 99 | /** 100 | * Custom font family for this fragment. 101 | * Note: Currently applied as a top-level font on iOS NitroText. 102 | */ 103 | fontFamily?: string 104 | 105 | /** 106 | * The line height of the text. 107 | */ 108 | lineHeight?: number 109 | 110 | /** 111 | * Additional space between letters (kerning), in points. 112 | * Matches React Native Text's `letterSpacing` on iOS. 113 | */ 114 | letterSpacing?: number 115 | 116 | /** 117 | * Horizontal text alignment applied to the whole block. 118 | */ 119 | textAlign?: TextAlign 120 | /** 121 | * Applies text transform to the content. 122 | */ 123 | textTransform?: TextTransform 124 | 125 | /** 126 | * Text decoration for underline/strikethrough. 127 | * Mirrors RN Text's `textDecorationLine`. 128 | */ 129 | textDecorationLine?: TextDecorationLine 130 | 131 | /** 132 | * Text decoration color. 133 | */ 134 | textDecorationColor?: string 135 | 136 | /** 137 | * Text decoration style (solid, double, dotted, dashed). 138 | */ 139 | textDecorationStyle?: TextDecorationStyle 140 | 141 | /** 142 | * Link URL (href attribute from tag). 143 | * When present, this fragment represents clickable link text. 144 | */ 145 | linkUrl?: string 146 | } 147 | 148 | /** 149 | * A menu item for the selection menu. 150 | */ 151 | export type MenuItem = { 152 | title: string 153 | action: () => void 154 | } 155 | 156 | /** 157 | * Supported renderers for rich text parsing. 158 | */ 159 | export type Renderer = 'html' | 'plaintext' 160 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-nitro-text", 3 | "version": "1.1.0", 4 | "description": "A Text component that is much richer and performant for both iOS and Android.", 5 | "main": "./lib/commonjs/index.js", 6 | "module": "./lib/module/index.js", 7 | "types": "./lib/typescript/src/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "scripts": { 11 | "typecheck": "tsc --noEmit", 12 | "lint": "eslint src", 13 | "lint:fix": "eslint src --fix", 14 | "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,md}\"", 15 | "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,json,md}\"", 16 | "clean": "git clean -dfX", 17 | "release": "semantic-release", 18 | "build": "bun run typecheck && bob build", 19 | "codegen": "nitrogen --logLevel=\"debug\" && bun run build", 20 | "postcodegen": "bun --cwd example pod", 21 | "test": "jest" 22 | }, 23 | "keywords": [ 24 | "react-native", 25 | "react-native-nitro-text" 26 | ], 27 | "files": [ 28 | "src", 29 | "react-native.config.js", 30 | "lib", 31 | "nitrogen", 32 | "cpp", 33 | "nitro.json", 34 | "android/build.gradle", 35 | "android/fix-prefab.gradle", 36 | "android/gradle.properties", 37 | "android/CMakeLists.txt", 38 | "android/src", 39 | "ios/**/*.h", 40 | "ios/**/*.hpp", 41 | "ios/**/*.m", 42 | "ios/**/*.mm", 43 | "ios/**/*.cpp", 44 | "ios/**/*.swift", 45 | "app.plugin.js", 46 | "*.podspec", 47 | "README.md" 48 | ], 49 | "workspaces": [ 50 | "example" 51 | ], 52 | "repository": "https://github.com/patrickkabwe/react-native-nitro-text.git", 53 | "author": "Patrick Kabwe", 54 | "license": "MIT", 55 | "bugs": "https://github.com/patrickkabwe/react-native-nitro-text/issues", 56 | "homepage": "https://github.com/patrickkabwe/react-native-nitro-text#readme", 57 | "publishConfig": { 58 | "access": "public", 59 | "registry": "https://registry.npmjs.org/" 60 | }, 61 | "devDependencies": { 62 | "@eslint/js": "^9.38.0", 63 | "@jamesacarr/eslint-formatter-github-actions": "^0.2.0", 64 | "@react-native/eslint-config": "0.81.4", 65 | "@semantic-release/changelog": "^6.0.3", 66 | "@semantic-release/git": "^10.0.1", 67 | "@types/jest": "^29.5.12", 68 | "@types/react": "19.1.0", 69 | "@typescript-eslint/eslint-plugin": "^8.46.2", 70 | "@typescript-eslint/parser": "^8.46.2", 71 | "conventional-changelog-conventionalcommits": "^9.1.0", 72 | "eslint": "^9.38.0", 73 | "eslint-config-prettier": "^10.1.8", 74 | "eslint-plugin-prettier": "^5.5.4", 75 | "eslint-plugin-react": "^7.37.5", 76 | "eslint-plugin-react-hooks": "^7.0.1", 77 | "eslint-plugin-react-native": "^5.0.0", 78 | "nitrogen": "^0.31.9", 79 | "prettier": "^3.6.2", 80 | "react": "19.1.0", 81 | "react-native": "0.81.4", 82 | "react-native-builder-bob": "^0.37.0", 83 | "react-native-nitro-modules": "^0.31.9", 84 | "semantic-release": "^24.2.8", 85 | "typescript": "^5.8.3", 86 | "typescript-eslint": "^8.46.2", 87 | "jest": "^29.7.0" 88 | }, 89 | "peerDependencies": { 90 | "react": "*", 91 | "react-native": "*", 92 | "react-native-nitro-modules": "*" 93 | }, 94 | "prettier": { 95 | "quoteProps": "consistent", 96 | "singleQuote": true, 97 | "tabWidth": 3, 98 | "trailingComma": "es5", 99 | "useTabs": false, 100 | "semi": false 101 | }, 102 | "react-native-builder-bob": { 103 | "source": "src", 104 | "output": "lib", 105 | "targets": [ 106 | "commonjs", 107 | "module", 108 | [ 109 | "typescript", 110 | { 111 | "project": "tsconfig.json" 112 | } 113 | ] 114 | ] 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/NitroText-Swift-Cxx-Umbrella.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// NitroText-Swift-Cxx-Umbrella.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | // Forward declarations of C++ defined types 11 | // Forward declaration of `DynamicTypeRamp` to properly resolve imports. 12 | namespace margelo::nitro::nitrotext { enum class DynamicTypeRamp; } 13 | // Forward declaration of `EllipsizeMode` to properly resolve imports. 14 | namespace margelo::nitro::nitrotext { enum class EllipsizeMode; } 15 | // Forward declaration of `FontStyle` to properly resolve imports. 16 | namespace margelo::nitro::nitrotext { enum class FontStyle; } 17 | // Forward declaration of `FontWeight` to properly resolve imports. 18 | namespace margelo::nitro::nitrotext { enum class FontWeight; } 19 | // Forward declaration of `Fragment` to properly resolve imports. 20 | namespace margelo::nitro::nitrotext { struct Fragment; } 21 | // Forward declaration of `HybridNitroTextSpec` to properly resolve imports. 22 | namespace margelo::nitro::nitrotext { class HybridNitroTextSpec; } 23 | // Forward declaration of `LineBreakStrategyIOS` to properly resolve imports. 24 | namespace margelo::nitro::nitrotext { enum class LineBreakStrategyIOS; } 25 | // Forward declaration of `MenuItem` to properly resolve imports. 26 | namespace margelo::nitro::nitrotext { struct MenuItem; } 27 | // Forward declaration of `Renderer` to properly resolve imports. 28 | namespace margelo::nitro::nitrotext { enum class Renderer; } 29 | // Forward declaration of `TextAlign` to properly resolve imports. 30 | namespace margelo::nitro::nitrotext { enum class TextAlign; } 31 | // Forward declaration of `TextDecorationLine` to properly resolve imports. 32 | namespace margelo::nitro::nitrotext { enum class TextDecorationLine; } 33 | // Forward declaration of `TextDecorationStyle` to properly resolve imports. 34 | namespace margelo::nitro::nitrotext { enum class TextDecorationStyle; } 35 | // Forward declaration of `TextLayoutEvent` to properly resolve imports. 36 | namespace margelo::nitro::nitrotext { struct TextLayoutEvent; } 37 | // Forward declaration of `TextLayout` to properly resolve imports. 38 | namespace margelo::nitro::nitrotext { struct TextLayout; } 39 | // Forward declaration of `TextTransform` to properly resolve imports. 40 | namespace margelo::nitro::nitrotext { enum class TextTransform; } 41 | 42 | // Include C++ defined types 43 | #include "DynamicTypeRamp.hpp" 44 | #include "EllipsizeMode.hpp" 45 | #include "FontStyle.hpp" 46 | #include "FontWeight.hpp" 47 | #include "Fragment.hpp" 48 | #include "HybridNitroTextSpec.hpp" 49 | #include "LineBreakStrategyIOS.hpp" 50 | #include "MenuItem.hpp" 51 | #include "Renderer.hpp" 52 | #include "TextAlign.hpp" 53 | #include "TextDecorationLine.hpp" 54 | #include "TextDecorationStyle.hpp" 55 | #include "TextLayout.hpp" 56 | #include "TextLayoutEvent.hpp" 57 | #include "TextTransform.hpp" 58 | #include 59 | #include 60 | #include 61 | #include 62 | #include 63 | 64 | // C++ helpers for Swift 65 | #include "NitroText-Swift-Cxx-Bridge.hpp" 66 | 67 | // Common C++ types used in Swift 68 | #include 69 | #include 70 | #include 71 | #include 72 | 73 | // Forward declarations of Swift defined types 74 | // Forward declaration of `HybridNitroTextSpec_cxx` to properly resolve imports. 75 | namespace NitroText { class HybridNitroTextSpec_cxx; } 76 | 77 | // Include Swift defined types 78 | #if __has_include("NitroText-Swift.h") 79 | // This header is generated by Xcode/Swift on every app build. 80 | // If it cannot be found, make sure the Swift module's name (= podspec name) is actually "NitroText". 81 | #include "NitroText-Swift.h" 82 | // Same as above, but used when building with frameworks (`use_frameworks`) 83 | #elif __has_include() 84 | #include 85 | #else 86 | #error NitroText's autogenerated Swift header cannot be found! Make sure the Swift module's name (= podspec name) is actually "NitroText", and try building the app first. 87 | #endif 88 | -------------------------------------------------------------------------------- /example/ios/NitroTextExample/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/specs/nitro-text.nitro.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | HybridView, 3 | HybridViewMethods, 4 | HybridViewProps, 5 | } from 'react-native-nitro-modules' 6 | import type { 7 | DynamicTypeRamp, 8 | EllipsizeMode, 9 | Fragment, 10 | LineBreakStrategyIOS, 11 | TextLayoutEvent, 12 | MenuItem, 13 | Renderer, 14 | } from '../types' 15 | 16 | export interface NitroTextProps 17 | extends HybridViewProps, 18 | Omit { 19 | /** 20 | * The fragments of the text. 21 | */ 22 | fragments?: Fragment[] 23 | 24 | /** 25 | * Renderer for parsing rich text content from string children. 26 | * When specified, the string children are parsed by a purpose-built, zero-allocation parser: 27 | * - 'html': Parses HTML tags, inline CSS styles, ` 51 | * 52 | * 53 | * Bold and red from stylesheet 54 | * Title text 55 | * 56 | * 57 | * 58 | * ``` 59 | * 60 | * When not provided, content is treated as plain text or React children (nested NitroText components). 61 | */ 62 | renderer?: Renderer 63 | 64 | /** 65 | * Selectable text. 66 | */ 67 | selectable?: boolean 68 | 69 | /** 70 | * If true, text respects system font scaling (Dynamic Type). 71 | * Matches React Native Text's allowFontScaling. Defaults to true. 72 | */ 73 | allowFontScaling?: boolean 74 | 75 | /** 76 | * Controls where to truncate text when numberOfLines is set. 77 | * Defaults to 'tail' like RN Text. 78 | */ 79 | ellipsizeMode?: EllipsizeMode 80 | 81 | /** 82 | * Limits the text to a maximum number of lines. Truncation behavior is 83 | * controlled via `ellipsizeMode`. Matches React Native Text's `numberOfLines`. 84 | */ 85 | numberOfLines?: number 86 | 87 | /** 88 | * iOS-only line breaking strategy applied when wrapping lines. 89 | * Mirrors React Native's `lineBreakStrategyIOS`. 90 | * - 'standard' (default): Use Apple's standard strategies 91 | * - 'hangul-word': Prioritize Hangul word boundaries 92 | * - 'push-out': Push glyphs out to avoid breaks 93 | * - 'none': Disable special strategies 94 | */ 95 | lineBreakStrategyIOS?: LineBreakStrategyIOS 96 | 97 | /** 98 | * iOS Dynamic Type ramp. Selects the UIFontMetrics text style used for scaling. 99 | * Matches React Native's Text `dynamicTypeRamp`. Defaults to body 100 | */ 101 | dynamicTypeRamp?: DynamicTypeRamp 102 | 103 | /** 104 | * Caps the Dynamic Type scaling factor when `allowFontScaling` is true. 105 | * >= 1 to enforce a maximum multiplier; omit/undefined means no cap. 106 | */ 107 | maxFontSizeMultiplier?: number 108 | 109 | /** 110 | * iOS: Shrink text to fit within the container width. 111 | */ 112 | adjustsFontSizeToFit?: boolean 113 | 114 | /** 115 | * iOS: The smallest scale allowed when shrinking. 116 | * Range 0.01–1.0. Only used when `adjustsFontSizeToFit` is true. 117 | */ 118 | minimumFontScale?: number 119 | 120 | /** 121 | * Add custom menu items to the selection menu. 122 | */ 123 | menus?: MenuItem[] 124 | 125 | /** 126 | * The onTextLayout callback. Used to measure the layout of the text. 127 | */ 128 | onTextLayout?: (layout: TextLayoutEvent) => void 129 | 130 | /** 131 | * Called after a tap completes successfully. 132 | */ 133 | onPress?: () => void 134 | 135 | /** 136 | * Called when a press begins (touch down). 137 | */ 138 | onPressIn?: () => void 139 | 140 | /** 141 | * Called when a press ends (touch up/cancel). 142 | */ 143 | onPressOut?: () => void 144 | } 145 | 146 | export interface NitroTextMethods extends HybridViewMethods {} 147 | 148 | export type NitroText = HybridView< 149 | NitroTextProps, 150 | NitroTextMethods, 151 | { ios: 'swift' } 152 | > 153 | -------------------------------------------------------------------------------- /ios/NitroTextImpl+Fragment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NitroTextImpl+Fragment.swift 3 | // Pods 4 | // 5 | // Extracted helpers for fragment/default merging and font mapping. 6 | // 7 | 8 | import UIKit 9 | 10 | extension NitroTextImpl { 11 | struct FragmentTopDefaults { 12 | let fontSize: Double? 13 | let fontWeight: FontWeight? 14 | let fontColor: String? 15 | let fontStyle: FontStyle? 16 | let fontFamily: String? 17 | let lineHeight: Double? 18 | let letterSpacing: Double? 19 | let textAlign: TextAlign? 20 | let textTransform: TextTransform? 21 | let textDecorationLine: TextDecorationLine? 22 | let textDecorationColor: String? 23 | let textDecorationStyle: TextDecorationStyle? 24 | let selectionColor: String? 25 | 26 | var hasApplicableValues: Bool { 27 | if fontSize != nil { return true } 28 | if fontWeight != nil { return true } 29 | if let fc = fontColor, !fc.isEmpty { return true } 30 | if fontStyle != nil { return true } 31 | if let ff = fontFamily, !ff.isEmpty { return true } 32 | if let lh = lineHeight, lh > 0 { return true } 33 | if letterSpacing != nil { return true } 34 | if textAlign != nil { return true } 35 | if textTransform != nil { return true } 36 | if textDecorationLine != nil { return true } 37 | if let tdc = textDecorationColor, !tdc.isEmpty { return true } 38 | if textDecorationStyle != nil { return true } 39 | if let sc = selectionColor, !sc.isEmpty { return true } 40 | 41 | return false 42 | } 43 | } 44 | 45 | func apply(fragments: [Fragment]?, text: String?, top: FragmentTopDefaults) { 46 | if let t = text, (fragments == nil || fragments!.isEmpty), !top.hasApplicableValues { 47 | nitroTextView?.text = t 48 | return 49 | } 50 | 51 | guard let fragments, !fragments.isEmpty else { 52 | if let t = text { 53 | let single = Fragment( 54 | text: t, 55 | selectionColor: top.selectionColor, 56 | fontSize: top.fontSize, 57 | fontWeight: top.fontWeight, 58 | fontColor: top.fontColor, 59 | fragmentBackgroundColor: nil, 60 | fontStyle: top.fontStyle, 61 | fontFamily: top.fontFamily, 62 | lineHeight: top.lineHeight, 63 | letterSpacing: top.letterSpacing, 64 | textAlign: top.textAlign, 65 | textTransform: top.textTransform, 66 | textDecorationLine: top.textDecorationLine, 67 | textDecorationColor: top.textDecorationColor, 68 | textDecorationStyle: top.textDecorationStyle, 69 | linkUrl: nil 70 | ) 71 | setFragments([single]) 72 | } else { 73 | setFragments(nil) 74 | } 75 | return 76 | } 77 | 78 | if !top.hasApplicableValues, fragments.allSatisfy({ $0.text != nil }) { 79 | setFragments(fragments) 80 | return 81 | } 82 | 83 | // Merge top-level defaults into each fragment 84 | var merged: [Fragment] = [] 85 | merged.reserveCapacity(fragments.count) 86 | 87 | for var frag in fragments { 88 | mergeTop(into: &frag, with: top) 89 | merged.append(frag) 90 | } 91 | setFragments(merged) 92 | } 93 | 94 | // MARK: - Private Merge Helpers 95 | 96 | private func mergeTop(into frag: inout Fragment, with top: FragmentTopDefaults) { 97 | if frag.text == nil { frag.text = "" } 98 | 99 | if frag.fontSize == nil, let v = top.fontSize { frag.fontSize = v } 100 | if frag.fontWeight == nil, let v = top.fontWeight { frag.fontWeight = v } 101 | if frag.fontStyle == nil, let v = top.fontStyle { frag.fontStyle = v } 102 | if frag.letterSpacing == nil, let v = top.letterSpacing { frag.letterSpacing = v } 103 | if frag.textAlign == nil, let v = top.textAlign { frag.textAlign = v } 104 | if frag.textTransform == nil, let v = top.textTransform { frag.textTransform = v } 105 | if frag.textDecorationLine == nil, let v = top.textDecorationLine { frag.textDecorationLine = v } 106 | if frag.textDecorationStyle == nil, let v = top.textDecorationStyle { frag.textDecorationStyle = v } 107 | if frag.fontFamily == nil, let v = top.fontFamily, !v.isEmpty { frag.fontFamily = v } 108 | if frag.fontColor == nil, let v = top.fontColor, !v.isEmpty { frag.fontColor = v } 109 | if frag.selectionColor == nil, let v = top.selectionColor, !v.isEmpty { frag.selectionColor = v } 110 | if frag.textDecorationColor == nil, let v = top.textDecorationColor, !v.isEmpty { frag.textDecorationColor = v } 111 | if frag.lineHeight == nil, let v = top.lineHeight, v > 0 { frag.lineHeight = v } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

react-native-nitro-text

3 |
4 | 5 |

6 | A Text component that is much richer and performant for both iOS and Android. 7 |

8 | 9 | 10 |
11 | 12 | https://github.com/user-attachments/assets/57f56b3f-3988-4235-af83-a5f2cfd82121 13 | 14 |
15 | 16 |
17 | 18 | [![npm version](https://img.shields.io/npm/v/react-native-nitro-text?style=for-the-badge)](https://www.npmjs.org/package/react-native-nitro-text) 19 | [![Discord](https://img.shields.io/badge/Discord-Join%20Server-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/7KXUyHjz) 20 | [![npm downloads](https://img.shields.io/npm/dt/react-native-nitro-text.svg?style=for-the-badge)](https://www.npmjs.org/package/react-native-nitro-text) 21 | [![npm downloads](https://img.shields.io/npm/dm/react-native-nitro-text.svg?style=for-the-badge)](https://www.npmjs.org/package/react-native-nitro-text) 22 | [![mit licence](https://img.shields.io/dub/l/vibe-d.svg?style=for-the-badge)](https://github.com/patrickkabwe/react-native-nitro-text/blob/main/LICENSE) 23 | 24 |
25 | 26 | --- 27 | 28 | ## Features 29 | 30 | - Works on both iOS and Android(currently fallback to RN `Text` on Android) 31 | - Native iOS rendering with smooth selection. 32 | - Nested fragments merge into a single native text view 33 | - Rendering Markdown and HTML (coming soon). 34 | - Supports only the New Architecture 35 | 36 | ## Requirements 37 | 38 | - React Native v0.78.0 or higher (Fabric/Nitro Views) 39 | - Node 18+ (Node 20+ recommended) 40 | 41 | ## Installation 42 | 43 | ```bash 44 | yarn add react-native-nitro-text react-native-nitro-modules 45 | ``` 46 | 47 | iOS 48 | 49 | ```bash 50 | cd ios && pod install && cd .. 51 | ``` 52 | 53 | That’s it. You can now use the `NitroText` component in your app. 54 | 55 | ## Usage 56 | 57 | ```tsx 58 | import { NitroText as Text } from 'react-native-nitro-text' 59 | 60 | export function Title() { 61 | return ( 62 | 63 | 🚀 NitroText Showcase 64 | 65 | ) 66 | } 67 | ``` 68 | 69 | ## Selection 70 | 71 | iOS uses native selection. On Android, NitroText currently falls back to React Native `Text`. 72 | 73 | ```tsx 74 | import { NitroText as Text } from 'react-native-nitro-text' 75 | 76 | export function SelectionExample() { 77 | return ( 78 | 79 | Long-press to select this text. NitroText supports smooth selection, 80 | even with inline styles and 81 | longer paragraphs. 82 | 83 | ) 84 | } 85 | ``` 86 | 87 | ## HTML rendering 88 | 89 | NitroText can parse HTML string children and inline CSS when you pass `renderer="html"`. 90 | 91 | ```tsx 92 | import { NitroText } from 'react-native-nitro-text' 93 | 94 | export function HtmlExample() { 95 | const html = ` 96 |
97 |

Renderer demo

98 |

This text comes from HTML with semantic tags.

99 |

Inline CSS works too.

100 |
101 | ` 102 | 103 | return {html} 104 | } 105 | ``` 106 | 107 | ## Custom selection menu 108 | 109 | NitroText supports custom menu items that appear when text is selected. Pass a `menus` prop with an array of menu items, each containing a `title` and `action` callback. 110 | 111 | ```tsx 112 | import { NitroText } from 'react-native-nitro-text' 113 | import { useMemo } from 'react' 114 | 115 | export function MenuExample() { 116 | const menus = useMemo( 117 | () => [ 118 | { 119 | title: 'Copy', 120 | action: () => console.log('Copy action') 121 | }, 122 | { 123 | title: 'Share', 124 | action: () => console.log('Share action') 125 | }, 126 | { 127 | title: 'Translate', 128 | action: () => console.log('Translate action') 129 | }, 130 | ], 131 | [] 132 | ) 133 | 134 | return ( 135 | 136 | Select this text to see custom menu options appear in the selection menu. 137 | 138 | ) 139 | } 140 | ``` 141 | 142 | ## Platform Support 143 | 144 | - iOS 145 | - Android - At the moment `NitroText` fallback to RN `Text`. 146 | 147 | ## Why NitroText? 148 | 149 | Custom native text view with minimal JS overhead and native iOS selection. Great for heavy/nested styled text and large lists. It's a drop-in replacement for RN `Text` component. 150 | 151 | ## Development 152 | 153 | - `bun run build` — typecheck and build the package 154 | - `bun run codegen` — regenerate codegen outputs 155 | - Example app in `example/` 156 | 157 | ## Credits 158 | 159 | Bootstrapped with [create-nitro-module](https://github.com/patrickkabwe/create-nitro-module). 160 | 161 | ## Contributing 162 | 163 | PRs welcome! Please open an issue first for major changes. 164 | 165 | > 💬 For quick support, join our [Discord channel](https://discord.gg/7KXUyHjz) 166 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "org.jetbrains.kotlin.android" 3 | apply plugin: "com.facebook.react" 4 | 5 | /** 6 | * This is the configuration block to customize your React Native Android app. 7 | * By default you don't need to apply any configuration, just uncomment the lines you need. 8 | */ 9 | react { 10 | /* Folders */ 11 | // The root of your project, i.e. where "package.json" lives. Default is '../..' 12 | // root = file("../../") 13 | // The folder where the react-native NPM package is. Default is ../../node_modules/react-native 14 | reactNativeDir = file("../../../node_modules/react-native") 15 | // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen 16 | codegenDir = file("../../../node_modules/@react-native/codegen") 17 | // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js 18 | cliFile = file("../../../node_modules/react-native/cli.js") 19 | 20 | /* Variants */ 21 | // The list of variants to that are debuggable. For those we're going to 22 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'. 23 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. 24 | // debuggableVariants = ["liteDebug", "prodDebug"] 25 | 26 | /* Bundling */ 27 | // A list containing the node command and its flags. Default is just 'node'. 28 | // nodeExecutableAndArgs = ["node"] 29 | // 30 | // The command to run when bundling. By default is 'bundle' 31 | // bundleCommand = "ram-bundle" 32 | // 33 | // The path to the CLI configuration file. Default is empty. 34 | // bundleConfig = file(../rn-cli.config.js) 35 | // 36 | // The name of the generated asset file containing your JS bundle 37 | // bundleAssetName = "MyApplication.android.bundle" 38 | // 39 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' 40 | // entryFile = file("../js/MyApplication.android.js") 41 | // 42 | // A list of extra flags to pass to the 'bundle' commands. 43 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle 44 | // extraPackagerArgs = [] 45 | 46 | /* Hermes Commands */ 47 | // The hermes compiler command to run. By default it is 'hermesc' 48 | hermesCommand = "$rootDir/../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc" 49 | // 50 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" 51 | // hermesFlags = ["-O", "-output-source-map"] 52 | 53 | /* Autolinking */ 54 | autolinkLibrariesWithApp() 55 | } 56 | 57 | /** 58 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode. 59 | */ 60 | def enableProguardInReleaseBuilds = false 61 | 62 | /** 63 | * The preferred build flavor of JavaScriptCore (JSC) 64 | * 65 | * For example, to use the international variant, you can use: 66 | * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+` 67 | * 68 | * The international variant includes ICU i18n library and necessary data 69 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 70 | * give correct results when using with locales other than en-US. Note that 71 | * this variant is about 6MiB larger per architecture than default. 72 | */ 73 | def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' 74 | 75 | android { 76 | ndkVersion rootProject.ext.ndkVersion 77 | buildToolsVersion rootProject.ext.buildToolsVersion 78 | compileSdk rootProject.ext.compileSdkVersion 79 | 80 | namespace "com.nitrotextexample" 81 | defaultConfig { 82 | applicationId "com.nitrotextexample" 83 | minSdkVersion rootProject.ext.minSdkVersion 84 | targetSdkVersion rootProject.ext.targetSdkVersion 85 | versionCode 1 86 | versionName "1.0" 87 | } 88 | signingConfigs { 89 | debug { 90 | storeFile file('debug.keystore') 91 | storePassword 'android' 92 | keyAlias 'androiddebugkey' 93 | keyPassword 'android' 94 | } 95 | } 96 | buildTypes { 97 | debug { 98 | signingConfig signingConfigs.debug 99 | } 100 | release { 101 | // Caution! In production, you need to generate your own keystore file. 102 | // see https://reactnative.dev/docs/signed-apk-android. 103 | signingConfig signingConfigs.debug 104 | minifyEnabled enableProguardInReleaseBuilds 105 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 106 | } 107 | } 108 | } 109 | 110 | dependencies { 111 | // The version of react-native is set by the React Native Gradle Plugin 112 | implementation("com.facebook.react:react-android") 113 | 114 | if (hermesEnabled.toBoolean()) { 115 | implementation("com.facebook.react:hermes-android") 116 | } else { 117 | implementation jscFlavor 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ios/NitroTextImpl+Attributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NitroTextImpl+Attributes.swift 3 | // Pods 4 | // 5 | // Attribute-building helpers for NitroTextImpl (colors, paragraph style, transforms). 6 | // 7 | 8 | import UIKit 9 | 10 | extension NitroTextImpl { 11 | func makeAttributes( 12 | for fragment: Fragment, 13 | defaultColor: UIColor 14 | ) -> [NSAttributedString.Key: Any] { 15 | var attrs: [NSAttributedString.Key: Any] = Dictionary(minimumCapacity: 8) 16 | 17 | let font = makeFont(for: fragment, defaultPointSize: nitroTextView?.font?.pointSize) 18 | attrs[.font] = font.value 19 | if font.isItalic { attrs[.obliqueness] = 0.2 } 20 | 21 | let para = makeParagraphStyle(for: fragment) 22 | attrs[.paragraphStyle] = para 23 | 24 | let color = resolveColor(for: fragment, defaultColor: defaultColor) 25 | attrs[.foregroundColor] = color 26 | 27 | // Background highlight per-fragment (to match RN Text backgroundColor on runs) 28 | if let bgColorString = fragment.fragmentBackgroundColor { 29 | if let bgParsed = parseColorCached(bgColorString) { 30 | attrs[.backgroundColor] = bgParsed 31 | } 32 | } 33 | 34 | if let spacing = fragment.letterSpacing { 35 | attrs[.kern] = spacing 36 | } 37 | 38 | // Underline / Strikethrough from textDecorationLine 39 | if let deco = fragment.textDecorationLine { 40 | let styleRaw = nsUnderlineStyle(from: fragment.textDecorationStyle) 41 | switch deco { 42 | case .underline: 43 | attrs[.underlineStyle] = styleRaw 44 | case .lineThrough: 45 | attrs[.strikethroughStyle] = styleRaw 46 | case .underlineLineThrough: 47 | attrs[.underlineStyle] = styleRaw 48 | attrs[.strikethroughStyle] = styleRaw 49 | case .none: 50 | break 51 | } 52 | } 53 | 54 | // Decoration color (applies to underline/strikethrough if present) 55 | if let decoColor = fragment.textDecorationColor { 56 | if let parsed = parseColorCached(decoColor) { 57 | attrs[.underlineColor] = parsed 58 | attrs[.strikethroughColor] = parsed 59 | } 60 | } 61 | 62 | if let urlString = fragment.linkUrl, !urlString.isEmpty { 63 | if let url = URL(string: urlString) { 64 | attrs[.link] = url 65 | } 66 | 67 | if fragment.fontColor == nil { 68 | if let fontColorString = fragment.fontColor, !fontColorString.isEmpty { 69 | if let customColor = parseColorCached(fontColorString) { 70 | attrs[.foregroundColor] = customColor 71 | } else { 72 | attrs[.foregroundColor] = UIColor.systemBlue 73 | } 74 | } else { 75 | attrs[.foregroundColor] = UIColor.systemBlue 76 | } 77 | } 78 | } 79 | 80 | return attrs 81 | } 82 | 83 | func resolveColor(for fragment: Fragment, defaultColor: UIColor) -> UIColor { 84 | if let value = fragment.fontColor { 85 | return parseColorCached(value) ?? defaultColor 86 | } 87 | return defaultColor 88 | } 89 | 90 | func parseColorCached(_ colorString: String) -> UIColor? { 91 | if let cached = colorCache[colorString] { 92 | return cached 93 | } 94 | 95 | if let parsed = ColorParser.parse(colorString) { 96 | colorCache[colorString] = parsed 97 | return parsed 98 | } 99 | 100 | return nil 101 | } 102 | 103 | private func nsUnderlineStyle(from style: TextDecorationStyle?) -> Int { 104 | guard let style else { return NSUnderlineStyle.single.rawValue } 105 | switch style { 106 | case .solid: 107 | return NSUnderlineStyle.single.rawValue 108 | case .double: 109 | return NSUnderlineStyle.double.rawValue 110 | case .dotted: 111 | return NSUnderlineStyle.patternDot.rawValue | NSUnderlineStyle.single.rawValue 112 | case .dashed: 113 | return NSUnderlineStyle.patternDash.rawValue | NSUnderlineStyle.single.rawValue 114 | } 115 | } 116 | 117 | func transform(_ text: String, with fragment: Fragment) -> String { 118 | let textTransform: TextTransform = { 119 | if let ft = fragment.textTransform { 120 | switch ft { 121 | case .uppercase: return .uppercase 122 | case .lowercase: return .lowercase 123 | case .capitalize: return .capitalize 124 | case .none: return .none 125 | } 126 | } 127 | return currentTransform 128 | }() 129 | 130 | switch textTransform { 131 | case .uppercase: return text.uppercased() 132 | case .lowercase: return text.lowercased() 133 | case .capitalize: return text.capitalized 134 | case .none: return text 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/nitro-text.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext, useMemo } from 'react' 2 | import { 3 | Platform, 4 | Text, 5 | type TextLayoutEvent, 6 | type TextProps, 7 | unstable_TextAncestorContext, 8 | } from 'react-native' 9 | 10 | import { 11 | callback, 12 | getHostComponent, 13 | type HybridRef, 14 | } from 'react-native-nitro-modules' 15 | import NitroTextConfig from '../nitrogen/generated/shared/json/NitroTextConfig.json' 16 | import type { NitroTextMethods, NitroTextProps } from './specs/nitro-text.nitro' 17 | import { 18 | flattenChildrenToFragments, 19 | getStyleProps, 20 | styleToFragment, 21 | } from './utils' 22 | import { renderStringChildren } from './renderers' 23 | 24 | export type NitroTextRef = HybridRef 25 | 26 | const NitroTextView = getHostComponent( 27 | 'NitroText', 28 | () => NitroTextConfig 29 | ) 30 | 31 | type NitroTextPropsWithEvents = Pick< 32 | NitroTextProps, 33 | | 'onTextLayout' 34 | | 'onPress' 35 | | 'onPressIn' 36 | | 'onPressOut' 37 | | 'menus' 38 | | 'renderer' 39 | | 'maxFontSizeMultiplier' 40 | > & 41 | Omit 42 | 43 | let TextAncestorContext = unstable_TextAncestorContext 44 | if ( 45 | Platform.constants.reactNativeVersion.major === 0 && 46 | Platform.constants.reactNativeVersion.minor < 81 47 | ) { 48 | TextAncestorContext = require('react-native/Libraries/Text/TextAncestor') 49 | } 50 | export const NitroText = (props: NitroTextPropsWithEvents) => { 51 | const isInsideRNText = useContext(TextAncestorContext) 52 | const { 53 | style, 54 | renderer, 55 | children, 56 | selectable, 57 | selectionColor, 58 | maxFontSizeMultiplier, 59 | onTextLayout, 60 | onPress, 61 | onPressIn, 62 | onPressOut, 63 | onLongPress, 64 | ...rest 65 | } = props 66 | 67 | const isStringChildren = typeof children === 'string' 68 | const isSimpleText = isStringChildren || typeof children === 'number' 69 | 70 | const topStyles = useMemo(() => { 71 | if (!style) return {} 72 | return styleToFragment(style) 73 | }, [style]) 74 | 75 | const parsedFragments = useMemo(() => { 76 | if (!renderer || !isStringChildren) return undefined 77 | const result = renderStringChildren(children, renderer, topStyles) 78 | return result.fragments 79 | }, [renderer, children, isStringChildren, topStyles]) 80 | 81 | const fragments = useMemo(() => { 82 | if (parsedFragments !== undefined) return parsedFragments 83 | if (isSimpleText) return [] 84 | return flattenChildrenToFragments(children, style) 85 | }, [parsedFragments, children, style, isSimpleText]) 86 | 87 | const styleProps = useMemo(() => getStyleProps(topStyles), [topStyles]) 88 | 89 | const onRNTextLayout = useCallback( 90 | (e: TextLayoutEvent) => { 91 | onTextLayout?.(e.nativeEvent) 92 | }, 93 | [onTextLayout] 94 | ) 95 | 96 | if (isInsideRNText || Platform.OS === 'android') { 97 | return ( 98 | 109 | {children} 110 | 111 | ) 112 | } 113 | 114 | if (renderer && isStringChildren) { 115 | return ( 116 | 129 | ) 130 | } 131 | 132 | if (isSimpleText) { 133 | return ( 134 | 147 | ) 148 | } 149 | 150 | return ( 151 | 164 | ) 165 | } 166 | 167 | NitroText.displayName = 'NitroText' 168 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/TextLayout.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// TextLayout.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | #if __has_include() 16 | #include 17 | #else 18 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 19 | #endif 20 | #if __has_include() 21 | #include 22 | #else 23 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 24 | #endif 25 | 26 | 27 | 28 | #include 29 | 30 | namespace margelo::nitro::nitrotext { 31 | 32 | /** 33 | * A struct which can be represented as a JavaScript object (TextLayout). 34 | */ 35 | struct TextLayout { 36 | public: 37 | std::string text SWIFT_PRIVATE; 38 | double x SWIFT_PRIVATE; 39 | double y SWIFT_PRIVATE; 40 | double width SWIFT_PRIVATE; 41 | double height SWIFT_PRIVATE; 42 | double descender SWIFT_PRIVATE; 43 | double capHeight SWIFT_PRIVATE; 44 | double ascender SWIFT_PRIVATE; 45 | double xHeight SWIFT_PRIVATE; 46 | 47 | public: 48 | TextLayout() = default; 49 | explicit TextLayout(std::string text, double x, double y, double width, double height, double descender, double capHeight, double ascender, double xHeight): text(text), x(x), y(y), width(width), height(height), descender(descender), capHeight(capHeight), ascender(ascender), xHeight(xHeight) {} 50 | }; 51 | 52 | } // namespace margelo::nitro::nitrotext 53 | 54 | namespace margelo::nitro { 55 | 56 | // C++ TextLayout <> JS TextLayout (object) 57 | template <> 58 | struct JSIConverter final { 59 | static inline margelo::nitro::nitrotext::TextLayout fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { 60 | jsi::Object obj = arg.asObject(runtime); 61 | return margelo::nitro::nitrotext::TextLayout( 62 | JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "text")), 63 | JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "x")), 64 | JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "y")), 65 | JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "width")), 66 | JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "height")), 67 | JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "descender")), 68 | JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "capHeight")), 69 | JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "ascender")), 70 | JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "xHeight")) 71 | ); 72 | } 73 | static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitrotext::TextLayout& arg) { 74 | jsi::Object obj(runtime); 75 | obj.setProperty(runtime, "text", JSIConverter::toJSI(runtime, arg.text)); 76 | obj.setProperty(runtime, "x", JSIConverter::toJSI(runtime, arg.x)); 77 | obj.setProperty(runtime, "y", JSIConverter::toJSI(runtime, arg.y)); 78 | obj.setProperty(runtime, "width", JSIConverter::toJSI(runtime, arg.width)); 79 | obj.setProperty(runtime, "height", JSIConverter::toJSI(runtime, arg.height)); 80 | obj.setProperty(runtime, "descender", JSIConverter::toJSI(runtime, arg.descender)); 81 | obj.setProperty(runtime, "capHeight", JSIConverter::toJSI(runtime, arg.capHeight)); 82 | obj.setProperty(runtime, "ascender", JSIConverter::toJSI(runtime, arg.ascender)); 83 | obj.setProperty(runtime, "xHeight", JSIConverter::toJSI(runtime, arg.xHeight)); 84 | return obj; 85 | } 86 | static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { 87 | if (!value.isObject()) { 88 | return false; 89 | } 90 | jsi::Object obj = value.getObject(runtime); 91 | if (!nitro::isPlainObject(runtime, obj)) { 92 | return false; 93 | } 94 | if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "text"))) return false; 95 | if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "x"))) return false; 96 | if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "y"))) return false; 97 | if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "width"))) return false; 98 | if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "height"))) return false; 99 | if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "descender"))) return false; 100 | if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "capHeight"))) return false; 101 | if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "ascender"))) return false; 102 | if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "xHeight"))) return false; 103 | return true; 104 | } 105 | }; 106 | 107 | } // namespace margelo::nitro 108 | -------------------------------------------------------------------------------- /src/renderers/utils.ts: -------------------------------------------------------------------------------- 1 | import { FRAGMENT_STYLE_KEYS, HTML_ENTITY_MAP } from '../constants' 2 | import type { Fragment } from '../types' 3 | import type { AppendState, RenderResult } from './types' 4 | 5 | export function createState(): AppendState { 6 | return { 7 | fragments: [], 8 | plainText: '', 9 | } 10 | } 11 | 12 | export function finalizeState(state: AppendState): RenderResult { 13 | return { 14 | fragments: state.fragments, 15 | text: state.plainText, 16 | } 17 | } 18 | 19 | export function appendText( 20 | state: AppendState, 21 | text: string, 22 | style: Partial 23 | ) { 24 | if (!text) return 25 | state.plainText += text 26 | const last = state.fragments[state.fragments.length - 1] 27 | if (last && fragmentsShareStyle(last, style)) { 28 | last.text = (last.text || '') + text 29 | return 30 | } 31 | state.fragments.push({ ...style, text }) 32 | } 33 | 34 | export function fragmentsShareStyle( 35 | fragment: Fragment, 36 | style: Partial 37 | ): boolean { 38 | for (const key of FRAGMENT_STYLE_KEYS) { 39 | if (fragment[key] !== style[key]) { 40 | return false 41 | } 42 | } 43 | return true 44 | } 45 | 46 | export function mergeStyles( 47 | base: Partial, 48 | override?: Partial 49 | ): Partial { 50 | if (!override || !Object.keys(override).length) { 51 | return base 52 | } 53 | if (!base || !Object.keys(base).length) { 54 | return { ...override } 55 | } 56 | return { ...base, ...override } 57 | } 58 | 59 | export function trimTrailingNewlines(state: AppendState) { 60 | while (state.plainText.endsWith('\n')) { 61 | state.plainText = state.plainText.slice(0, -1) 62 | removeTrailingChar(state.fragments, '\n') 63 | } 64 | } 65 | 66 | export function trimTrailingWhitespace(state: AppendState) { 67 | const trimmed = state.plainText.trimEnd() 68 | if (trimmed.length < state.plainText.length) { 69 | const diff = state.plainText.length - trimmed.length 70 | let removed = 0 71 | for (let i = state.fragments.length - 1; i >= 0 && removed < diff; i--) { 72 | const frag = state.fragments[i] 73 | if (!frag) continue 74 | if (!frag.text) { 75 | state.fragments.pop() 76 | continue 77 | } 78 | const fragLen = frag.text.length 79 | const toRemove = Math.min(fragLen, diff - removed) 80 | frag.text = frag.text.slice(0, fragLen - toRemove) 81 | removed += toRemove 82 | if (!frag.text) { 83 | state.fragments.pop() 84 | } 85 | } 86 | state.plainText = trimmed 87 | } 88 | } 89 | 90 | function removeTrailingChar(fragments: Fragment[], char: string) { 91 | while (fragments.length) { 92 | const last = fragments[fragments.length - 1] 93 | if (!last) { 94 | break 95 | } 96 | if (!last.text) { 97 | fragments.pop() 98 | continue 99 | } 100 | if (last.text.endsWith(char)) { 101 | last.text = last.text.slice(0, -char.length) 102 | if (!last.text.length) { 103 | fragments.pop() 104 | continue 105 | } 106 | } 107 | break 108 | } 109 | } 110 | 111 | export function trimLeadingWhitespace(state: AppendState) { 112 | const trimmed = state.plainText.trimStart() 113 | if (trimmed.length < state.plainText.length) { 114 | const diff = state.plainText.length - trimmed.length 115 | let removed = 0 116 | while (state.fragments.length && removed < diff) { 117 | const first = state.fragments[0] 118 | if (!first) { 119 | break 120 | } 121 | if (!first.text) { 122 | state.fragments.shift() 123 | continue 124 | } 125 | const fragLen = first.text.length 126 | const toRemove = Math.min(fragLen, diff - removed) 127 | first.text = first.text.slice(toRemove) 128 | removed += toRemove 129 | if (!first.text) { 130 | state.fragments.shift() 131 | } 132 | } 133 | state.plainText = trimmed 134 | } 135 | } 136 | 137 | export function decodeEntities(text: string): string { 138 | if (!text || text.indexOf('&') === -1) return text 139 | return text.replace( 140 | /&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, 141 | (_, entity: string) => { 142 | if (entity.startsWith('#x') || entity.startsWith('#X')) { 143 | const code = parseInt(entity.slice(2), 16) 144 | if (!Number.isNaN(code)) return String.fromCodePoint(code) 145 | } else if (entity.startsWith('#')) { 146 | const code = parseInt(entity.slice(1), 10) 147 | if (!Number.isNaN(code)) return String.fromCodePoint(code) 148 | } else if (HTML_ENTITY_MAP[entity]) { 149 | return HTML_ENTITY_MAP[entity] 150 | } 151 | return '&' + entity + ';' 152 | } 153 | ) 154 | } 155 | 156 | export function collapseWhitespace(text: string): string { 157 | return text.replace(/\s+/g, ' ') 158 | } 159 | 160 | export function parseNumeric(value: string): number | undefined { 161 | if (!value) return undefined 162 | const numeric = parseFloat(value) 163 | return Number.isFinite(numeric) ? numeric : undefined 164 | } 165 | 166 | export function isZeroMargin(value: string): boolean { 167 | const trimmed = value.trim() 168 | if (trimmed === '0') return true 169 | const numeric = parseFloat(trimmed) 170 | return Number.isFinite(numeric) && numeric === 0 171 | } 172 | 173 | export function stripQuotes(value: string) { 174 | if (!value) return value 175 | let result = value.trim().replace(/^['"]+|['"]+$/g, '') 176 | result = result.replace(/["']/g, '') 177 | return result 178 | } 179 | --------------------------------------------------------------------------------