prawn-rtl-support 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da391affbc37a7e52a023f3644738dd006ffecc177374e980bb2c91735721f7f
4
- data.tar.gz: 8703e5e55ef4a427ab0246667eba0b5c38fbe3c8807439cf4122f060356bcac4
3
+ metadata.gz: 233930e38a41afb8a807bf1753e171aa3a8112bacd425c57c8b93c2aecd374f2
4
+ data.tar.gz: b6ea130b2b05f7ea1f002f7023fbd22b5338324e7f0a4695175310c37d6ebb99
5
5
  SHA512:
6
- metadata.gz: 29ebec9291006ad1b9d9f9eb862a067b2b26136caa8cbecbfe120ff96facf3dac647c13679c59e0a5553526d79fda6910965b27621eef75cf495ff7641b0c0ba
7
- data.tar.gz: d5c27dc0a9220f14d1d8bbbf5b68ed361bc2dfce024160ccf7af71e25a15de605a8c6fbeddbcde457a60cdfc19d20dcffca643d78e334e3bc7d1d3ae72187f0c
6
+ metadata.gz: 882f584327fb3d2cacddb945f46a37c07fc48f0c93e0b069fd3f567359f6df226b2a7ad1ed6f604907bc912228b2acfaf34a88f06adbcde58569d97c8428298e
7
+ data.tar.gz: 9602c40c2e4668188f1c8d91b79587a879a50207771b247f79ea7afd9721706a5310e21793e83453b275587b7b5dc5c4e70d2fbec43e8018e277b72a2d4f58cf
data/CHANGELOG.md ADDED
@@ -0,0 +1,67 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2025-09-04
9
+
10
+ ### ⚠️ Breaking Changes
11
+ - **ICU library requirement**: The gem now requires the ICU library to be installed on the system. This may affect macOS and Windows users who previously relied on twitter_cldr's bundled ICU data.
12
+ - **Linux**: Usually pre-installed
13
+ - **macOS**: Install via Homebrew: `brew install icu4c`
14
+ - **Windows**: May require manual ICU installation
15
+ - **Custom path**: Set `ICU_LIB_PATH` environment variable if ICU is installed in a non-standard location
16
+
17
+ ### Customer-Impacting Changes
18
+
19
+ #### Added
20
+ - **Extended platform support**: Added ARM64 architecture support in CI/CD pipeline
21
+
22
+ #### Changed
23
+ - **Dependencies**: Replaced twitter_cldr with direct FFI bindings (lighter dependency footprint, but requires system ICU library)
24
+ - **Performance**: Direct ICU integration provides better performance for text processing
25
+
26
+ ### Internal Changes
27
+
28
+ #### Testing & Quality
29
+ - Added comprehensive integration tests for BiDi functionality
30
+ - Extended CI matrix to include Ubuntu 22.04, 24.04 and ARM64 variants
31
+
32
+ ## [0.1.8] - 2025-09-04
33
+
34
+ ### Customer-Impacting Changes
35
+
36
+ #### Added
37
+ - **Ruby 2.7+ requirement**: Gem now explicitly requires Ruby 2.7 or higher
38
+ - **Better documentation**: All public APIs now have YARD documentation with examples
39
+
40
+ ### Internal Changes
41
+
42
+ #### Development & CI
43
+ - Migrated from Travis CI to GitHub Actions
44
+ - Added testing matrix for Ruby 2.7, 3.0, 3.1, 3.2, 3.3, and 3.4
45
+ - Added RuboCop linting to CI workflow
46
+ - Added Dependabot for automated dependency updates
47
+ - Added CLAUDE.md for AI-assisted development guidance
48
+ - Multi-factor authentication (MFA) is now required for gem publishers
49
+
50
+ #### Code Quality
51
+ - Added YARD documentation to all modules and classes
52
+ - Fixed some RuboCop violations
53
+ - Fixed typos
54
+ - Improved gemspec metadata (added source_code_uri, changelog_uri, bug_tracker_uri, documentation_uri)
55
+ - Moved development dependencies from gemspec to Gemfile
56
+ - Removed unnecessary `$LOAD_PATH` manipulation
57
+ - Internal file requires now use `require_relative` for faster loading
58
+ - Gemspec file inclusion no longer depends on git, works in any environment
59
+
60
+ #### Documentation
61
+ - Updated README with "Supported Languages" section
62
+ - Updated gemspec description to accurately reflect RTL language support
63
+ - Added comprehensive code examples in YARD documentation
64
+
65
+ ## [0.1.7] - 2020-05-10
66
+
67
+ For changes in previous versions, see the git commit history.
data/CLAUDE.md ADDED
@@ -0,0 +1,57 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This is prawn-rtl-support, a Ruby gem that provides bidirectional text (RTL/LTR) support for the Prawn PDF generation library. It enables proper rendering of Arabic and other right-to-left languages in PDFs by:
8
+ - Using Unicode Bidirectional Algorithm via ICU for text reordering
9
+ - Connecting Arabic letters properly for visual display
10
+ - Minimally patching Prawn's text rendering pipeline
11
+
12
+ ## Development Commands
13
+
14
+ ```bash
15
+ # Install dependencies
16
+ bundle install
17
+
18
+ # Run test suite
19
+ bundle exec rake spec
20
+ # or just
21
+ rake spec
22
+
23
+ # Run linting (RuboCop)
24
+ bundle exec rubocop
25
+
26
+ # Open console for experimentation
27
+ bin/console
28
+
29
+ # Build gem locally
30
+ bundle exec rake build
31
+
32
+ # Install gem to local machine
33
+ bundle exec rake install
34
+ ```
35
+
36
+ ## Architecture
37
+
38
+ The gem patches Prawn by prepending a module to `Prawn::Text::Formatted::Box#original_text`. The core functionality:
39
+
40
+ 1. **lib/prawn/rtl/support.rb**: Main entry point that patches Prawn::Text::Formatted::Box
41
+ 2. **lib/prawn/rtl/connector.rb**: Core RTL fixing logic with three main methods:
42
+ - `fix_rtl(string)`: Main public API that detects RTL text and processes it
43
+ - `connect(string)`: Applies Arabic letter connection rules
44
+ - `reorder(string)`: Uses ICU's Bidi algorithm to reorder text visually
45
+ 3. **lib/prawn/rtl/connector/logic.rb**: Arabic letter connection logic with character mapping tables for different forms (isolated, initial, medial, final)
46
+
47
+ The gem automatically activates when required - no configuration needed. It detects RTL text and only processes strings that contain RTL characters.
48
+
49
+ ## Key Dependencies
50
+
51
+ - **prawn ~> 2.2**: The PDF generation library being patched
52
+ - **ffi ~> 1.15**: Foreign Function Interface for Ruby to call ICU C library functions
53
+ - **ICU library**: System dependency (libicuuc) providing Unicode Bidirectional Algorithm implementation
54
+
55
+ ## Contributing
56
+
57
+ - Run tests `bundle exec rake` and RuboCop linting `bundle exec rubocop` before committing.
data/Gemfile CHANGED
@@ -1,4 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in prawn-rtl-support.gemspec
4
6
  gemspec
7
+
8
+ # Development dependencies
9
+ group :development, :test do
10
+ gem 'pry', '~> 0.12'
11
+ gem 'rake', '~> 13.0'
12
+ gem 'rspec', '~> 3.9'
13
+ gem 'rubocop', '1.80.2'
14
+ gem 'rubocop-performance', '1.25.0'
15
+ gem 'rubocop-rake', '0.7.1'
16
+ gem 'rubocop-rspec', '3.7.0'
17
+ gem 'rubocop-rubycw', '0.2.2'
18
+ gem 'rubocop-thread_safety', '0.7.3'
19
+ end
data/README.md CHANGED
@@ -1,15 +1,53 @@
1
1
  # Prawn::Rtl::Support
2
2
 
3
- [![Build Status](https://travis-ci.org/cropio/prawn-rtl-support.svg?branch=master)](https://travis-ci.org/cropio/prawn-rtl-support)
3
+ [![CI](https://github.com/prawn-rtl-support/prawn-rtl-support/actions/workflows/ci.yml/badge.svg)](https://github.com/prawn-rtl-support/prawn-rtl-support/actions/workflows/ci.yml)
4
4
 
5
- This gem provide bidirectional text support for Prawn. It uses Unicode Bidirectional Algorithm for displaying text from [TwitterCldr::Shared::Bidi](https://github.com/twitter/twitter-cldr-rb) and connect arabic letters using [Arabic Letter Connector](https://github.com/staii/arabic-letter-connector). Prawn patching is minimal, we patch only [`Prawn::Text::Formatted::Box#original_text`](https://github.com/prawnpdf/prawn/blob/master/lib/prawn/text/formatted/box.rb#L367).
5
+ This gem provides bidirectional text support for Prawn PDF generator. It uses the Unicode Bidirectional Algorithm via [ICU](http://site.icu-project.org/) (International Components for Unicode) for text reordering and implements Arabic letter shaping similar to [Arabic Letter Connector](https://github.com/staii/arabic-letter-connector). Prawn patching is minimal - we only patch [`Prawn::Text::Formatted::Box#original_text`](https://github.com/prawnpdf/prawn/blob/master/lib/prawn/text/formatted/box.rb#L367).
6
+
7
+ ## Supported Languages
8
+
9
+ - **Full support** (with contextual letter shaping):
10
+ - Arabic
11
+ - Persian/Farsi
12
+ - Urdu
13
+ - Other Arabic script languages
14
+
15
+ - **RTL support** (bidirectional text reordering):
16
+ - Hebrew
17
+ - Syriac
18
+ - Thaana
19
+ - Mixed LTR/RTL text
6
20
 
7
21
  ## Motivation
8
22
 
9
- Ruby and Rails internally provide unicode string normalization and store normalized letters inside. But Prawn don't connect arabic glyphs back and don't suport mixet LTR and RTL string. This gem add this suport.
23
+ Ruby and Rails internally provide Unicode string normalization. However, Prawn doesn't connect Arabic letters into their contextual forms and doesn't support mixed LTR and RTL strings. This gem adds this support.
10
24
 
11
- ## Acknowledgment
12
- This gem use same code as [Arabic Letter Connector](https://github.com/staii/arabic-letter-connector) by [@staii](https://github.com/staii) and therefore based on [Arabic-Prawn](https://rubygems.org/gems/Arabic-Prawn/versions/0.0.1) by Dynamix Solutions (Ahmed Nasser).
25
+ ## Requirements
26
+
27
+ ### ICU Library
28
+
29
+ Starting from version 0.2.0, this gem requires the ICU library to be installed on your system:
30
+
31
+ - **Linux**: Usually pre-installed. If not, install with:
32
+ ```bash
33
+ # Ubuntu/Debian
34
+ sudo apt-get install libicu-dev
35
+
36
+ # RHEL/CentOS/Fedora
37
+ sudo yum install libicu-devel
38
+ ```
39
+
40
+ - **macOS**: Install via Homebrew:
41
+ ```bash
42
+ brew install icu4c
43
+ ```
44
+
45
+ - **Windows**: Download and install ICU from the [official site](http://site.icu-project.org/download)
46
+
47
+ - **Custom installation path**: If ICU is installed in a non-standard location, set the `ICU_LIB_PATH` environment variable:
48
+ ```bash
49
+ export ICU_LIB_PATH=/custom/path/to/icu/lib
50
+ ```
13
51
 
14
52
  ## Installation
15
53
 
@@ -23,24 +61,30 @@ And that's all. Your Prawn is patched!
23
61
 
24
62
  Or install it yourself as:
25
63
 
26
- $ gem install prawn-rtl-support
64
+ ```shell
65
+ gem install prawn-rtl-support
66
+ ```
27
67
 
28
68
  ## Usage
29
69
 
30
- `prawn-rtl-support` provide method [`Prawn::Rtl::Connector#fix_rtl(string)`](https://github.com/cropio/prawn-rtl-support/blob/master/lib/prawn/rtl/connector.rb#L13) which reverse string and connect arabic letters.
70
+ `prawn-rtl-support` provide method [`Prawn::Rtl::Connector#fix_rtl(string)`](https://github.com/prawn-rtl-support/prawn-rtl-support/blob/master/lib/prawn/rtl/connector.rb#L13) which reverse string and connect arabic letters.
71
+
31
72
  Prawn patching is minimal, we patch only [`Prawn::Text::Formatted::Box#original_text`](https://github.com/prawnpdf/prawn/blob/master/lib/prawn/text/formatted/box.rb#L367).
32
73
 
33
74
  ## Development
34
75
 
76
+ Check [CLAUDE.md](CLAUDE.md) for more details about the architecture and development commands.
77
+
35
78
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
36
79
 
37
80
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
38
81
 
39
82
  ## Contributing
40
83
 
41
- Bug reports and pull requests are welcome on GitHub at https://github.com/cropio/prawn-rtl-support. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
42
-
84
+ Bug reports and pull requests are welcome on GitHub at https://github.com/prawn-rtl-support/prawn-rtl-support. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
43
85
 
86
+ ## Acknowledgment
87
+ This gem uses the same code as [Arabic Letter Connector](https://github.com/staii/arabic-letter-connector) by [@staii](https://github.com/staii) and therefore is based on [Arabic-Prawn](https://rubygems.org/gems/Arabic-Prawn/versions/0.0.1) by Dynamix Solutions (Ahmed Nasser).
44
88
 
45
89
  ## License
46
90
 
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbconfig'
4
+ require 'ffi'
5
+
6
+ module Prawn
7
+ module Rtl
8
+ # FFI BiDi wrapper for Unicode Bidirectional Algorithm support
9
+ #
10
+ # This module provides direct FFI bindings to ICU's ubidi functions
11
+ # for bidirectional text processing.
12
+ module Bidi
13
+ extend FFI::Library
14
+
15
+ class BiDiError < StandardError; end
16
+
17
+ # Detect platform
18
+ def self.platform
19
+ os = RbConfig::CONFIG['host_os']
20
+ case os
21
+ when /darwin/
22
+ :osx
23
+ when /linux/
24
+ :linux
25
+ when /bsd/
26
+ :bsd
27
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
28
+ :windows
29
+ else
30
+ :linux
31
+ end
32
+ end
33
+
34
+ # Search paths for ICU libraries
35
+ def self.search_paths
36
+ @search_paths ||=
37
+ if ENV['ICU_LIB_PATH']
38
+ [ENV['ICU_LIB_PATH']]
39
+ elsif FFI::Platform::IS_WINDOWS
40
+ ENV['PATH'].split(File::PATH_SEPARATOR)
41
+ else
42
+ [
43
+ '/usr/local/{lib64,lib}',
44
+ '/opt/local/{lib64,lib}',
45
+ '/opt/homebrew/{lib64,lib}',
46
+ '/usr/{lib64,lib}'
47
+ ] + Dir['/usr/lib/*-linux-gnu'] + Dir['/lib/*-linux-gnu']
48
+ end
49
+ end
50
+
51
+ # Find ICU library files
52
+ def self.find_icu_lib
53
+ candidates = []
54
+ lib_name = 'icuuc'
55
+
56
+ search_paths.each do |path|
57
+ Dir.glob(File.expand_path(path)).each do |dir|
58
+ # Try versioned libraries first (newer to older)
59
+ # ICU versions from 4.0 (2009) to potential future versions
60
+ 90.downto(4).each do |version|
61
+ case platform
62
+ when :osx
63
+ candidates << File.join(dir, "lib#{lib_name}.#{version}.dylib")
64
+ candidates << File.join(dir, "lib#{lib_name}.dylib")
65
+ when :windows
66
+ candidates << File.join(dir, "#{lib_name}#{version}.dll")
67
+ candidates << File.join(dir, "#{lib_name}.dll")
68
+ else
69
+ candidates << File.join(dir, "lib#{lib_name}.so.#{version}")
70
+ candidates << File.join(dir, "lib#{lib_name}.so")
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ # Find the first existing library
77
+ found = candidates.find { |path| File.exist?(path) }
78
+ found || ["lib#{lib_name}.so", "lib#{lib_name}.dylib", "#{lib_name}.dll", lib_name]
79
+ end
80
+
81
+ # Load the library
82
+ ffi_lib find_icu_lib
83
+
84
+ # Detect ICU version suffix by checking for a known function
85
+ def self.detect_icu_suffix
86
+ @detect_icu_suffix ||= begin
87
+ # Try common suffixes from newer to older versions
88
+ # Some versions use _4_2 format, others use _42 format
89
+ suffixes = [''] + 90.downto(4).flat_map { |v| ["_#{v}", "_#{v / 10}_#{v % 10}"] }
90
+
91
+ # Find suffix by checking if ubidi_open function exists
92
+ suffix = suffixes.find do |s|
93
+ # Try to find the function
94
+ func_name = :"ubidi_open#{s}"
95
+ ffi_libraries.any? do |lib|
96
+ lib.find_function(func_name.to_s)
97
+ end
98
+ rescue StandardError
99
+ false
100
+ end
101
+
102
+ suffix || ''
103
+ end
104
+ end
105
+
106
+ # Helper to attach function with detected suffix
107
+ def self.attach_icu_function(ruby_name, icu_name, args, return_type)
108
+ suffixed_name = "#{icu_name}#{detect_icu_suffix}"
109
+ attach_function ruby_name, suffixed_name.to_sym, args, return_type
110
+ end
111
+
112
+ # Constants from ubidi.h
113
+ UBIDI_DEFAULT_LTR = 0xfe
114
+ UBIDI_DEFAULT_RTL = 0xff
115
+ UBIDI_LTR = 0
116
+ UBIDI_RTL = 1
117
+ UBIDI_MIXED = 2
118
+ UBIDI_NEUTRAL = 3
119
+
120
+ # Reorder options
121
+ UBIDI_DO_MIRRORING = 2
122
+ UBIDI_OUTPUT_REVERSE = 16
123
+
124
+ # Attach ICU functions with version detection
125
+ attach_icu_function :ubidi_open, 'ubidi_open', [], :pointer
126
+ attach_icu_function :ubidi_close, 'ubidi_close', [:pointer], :void
127
+ attach_icu_function :ubidi_setPara, 'ubidi_setPara', %i[pointer pointer int32 uint8 pointer pointer], :void
128
+ attach_icu_function :ubidi_getDirection, 'ubidi_getDirection', [:pointer], :int
129
+ attach_icu_function :ubidi_getLength, 'ubidi_getLength', [:pointer], :int32
130
+ attach_icu_function :ubidi_writeReordered, 'ubidi_writeReordered', %i[pointer pointer int32 uint16 pointer],
131
+ :int32
132
+ attach_icu_function :ubidi_countRuns, 'ubidi_countRuns', %i[pointer pointer], :int32
133
+
134
+ # Reorders text according to the Unicode Bidirectional Algorithm
135
+ #
136
+ # @param text [String] the text to reorder
137
+ # @param direction [Symbol] :ltr, :rtl, or :auto (default)
138
+ # @return [String] the visually reordered text
139
+ def self.reorder(text, direction: :auto)
140
+ return text if text.nil? || text.empty?
141
+
142
+ # Convert direction to ubidi constant
143
+ para_level =
144
+ case direction
145
+ when :ltr then UBIDI_LTR
146
+ when :rtl then UBIDI_RTL
147
+ else UBIDI_DEFAULT_LTR
148
+ end
149
+
150
+ bidi = nil
151
+ begin
152
+ # Open BiDi object
153
+ bidi = ubidi_open
154
+ raise BiDiError, 'Failed to create BiDi object' if bidi.null?
155
+
156
+ # Convert string to UTF-16 for ICU
157
+ utf16_text = text.encode('UTF-16LE')
158
+ text_length = utf16_text.bytesize / 2
159
+
160
+ # Create buffer for UTF-16 string
161
+ text_buffer = FFI::MemoryPointer.new(:uint16, text_length + 1)
162
+ text_buffer.put_bytes(0, utf16_text)
163
+
164
+ # Error status
165
+ status = FFI::MemoryPointer.new(:int32)
166
+ status.put_int32(0, 0)
167
+
168
+ # Set paragraph
169
+ ubidi_setPara(bidi, text_buffer, text_length, para_level, nil, status)
170
+
171
+ error_code = status.get_int32(0)
172
+ raise BiDiError, "ubidi_setPara failed with error code: #{error_code}" if error_code.positive?
173
+
174
+ # Get required size for output
175
+ output_length = text_length * 2
176
+ output_buffer = FFI::MemoryPointer.new(:uint16, output_length)
177
+
178
+ # Write reordered text
179
+ written = ubidi_writeReordered(bidi, output_buffer, output_length, UBIDI_DO_MIRRORING, status)
180
+
181
+ error_code = status.get_int32(0)
182
+ raise BiDiError, "ubidi_writeReordered failed with error code: #{error_code}" if error_code.positive?
183
+
184
+ # Convert back from UTF-16 to UTF-8
185
+ result_bytes = output_buffer.get_bytes(0, written * 2)
186
+ result_bytes.force_encoding('UTF-16LE').encode('UTF-8')
187
+ ensure
188
+ ubidi_close(bidi) if bidi && !bidi.null?
189
+ end
190
+ end
191
+
192
+ # Checks if a string contains RTL characters
193
+ #
194
+ # @param text [String] the text to check
195
+ # @return [Boolean] true if the text contains RTL characters
196
+ def self.contains_rtl?(text)
197
+ return false if text.nil? || text.empty?
198
+
199
+ bidi = nil
200
+ begin
201
+ bidi = ubidi_open
202
+ return false if bidi.null?
203
+
204
+ utf16_text = text.encode('UTF-16LE')
205
+ text_length = utf16_text.bytesize / 2
206
+
207
+ text_buffer = FFI::MemoryPointer.new(:uint16, text_length + 1)
208
+ text_buffer.put_bytes(0, utf16_text)
209
+
210
+ status = FFI::MemoryPointer.new(:int32)
211
+ status.put_int32(0, 0)
212
+
213
+ ubidi_setPara(bidi, text_buffer, text_length, UBIDI_DEFAULT_LTR, nil, status)
214
+
215
+ return false if status.get_int32(0).positive?
216
+
217
+ direction = ubidi_getDirection(bidi)
218
+ [UBIDI_RTL, UBIDI_MIXED].include?(direction)
219
+ ensure
220
+ ubidi_close(bidi) if bidi && !bidi.null?
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -3,48 +3,82 @@
3
3
  module Prawn
4
4
  module Rtl
5
5
  module Connector
6
+ # Handles the logic for Arabic letter connection and contextual form selection.
7
+ #
8
+ # This module implements the core algorithm for determining which form
9
+ # (isolated, initial, medial, or final) an Arabic character should take
10
+ # based on its surrounding characters. It maintains a mapping of Arabic
11
+ # Unicode characters to their various contextual forms.
6
12
  module Logic
7
-
8
13
  @@charinfos = nil
9
14
 
15
+ # Represents information about an Arabic character and its contextual forms.
16
+ #
17
+ # Each Arabic letter can have up to four different forms depending on
18
+ # its position within a word and connection to surrounding letters.
10
19
  class CharacterInfo
11
- attr_accessor :common, :formatted
20
+ # @return [String] the base Unicode character
21
+ attr_accessor :common
22
+
23
+ # @return [Hash] the character's forms (:isolated, :final, :initial, :medial)
24
+ attr_accessor :formatted
12
25
 
26
+ # Initializes a new CharacterInfo instance.
27
+ #
28
+ # @param common [String] the base Unicode character
29
+ # @param isolated [String] the isolated form of the character
30
+ # @param final [String] the final form of the character
31
+ # @param initial [String] the initial form of the character
32
+ # @param medial [String] the medial form of the character
33
+ # @param connects [Boolean] whether this character connects to the next
34
+ # @param diacritic [Boolean] whether this character is a diacritic mark
13
35
  def initialize(common, isolated, final, initial, medial, connects, diacritic)
14
36
  @common = common
15
37
  @formatted = {
16
38
  isolated: isolated,
17
39
  final: final,
18
40
  initial: initial,
19
- medial: medial,
41
+ medial: medial
20
42
  }
21
43
  @connects = connects
22
44
  @diacritic = diacritic
23
45
  end
24
46
 
47
+ # Checks if this character connects to the following character.
48
+ #
49
+ # @return [Boolean] true if the character connects forward
25
50
  def connects?
26
51
  @connects
27
52
  end
28
53
 
54
+ # Checks if this character is a diacritic mark.
55
+ #
56
+ # @return [Boolean] true if the character is a diacritic
29
57
  def diacritic?
30
58
  @diacritic
31
59
  end
32
-
33
60
  end
34
61
 
35
- # Determine the form of the current character (:isolated, :initial, :medial,
62
+ # Determines the contextual form of an Arabic character.
63
+ #
64
+ # Determines the form of the current character (:isolated, :initial, :medial,
36
65
  # or :final), given the previous character and the next one. In Arabic, all
37
66
  # characters can connect with a previous character, but not all letters can
38
- # connect with the next character (this is determined by
39
- # CharacterInfo#connects?).
67
+ # connect with the next character (this is determined by CharacterInfo#connects?).
68
+ #
69
+ # @param previous_previous_char [String, nil] the character two positions before
70
+ # @param previous_char [String, nil] the character immediately before
71
+ # @param next_char [String, nil] the character immediately after
72
+ # @param next_next_char [String, nil] the character two positions after
73
+ # @return [Symbol] the contextual form (:isolated, :initial, :medial, or :final)
40
74
  def self.determine_form(previous_previous_char, previous_char, next_char, next_next_char)
41
75
  charinfos = self.charinfos
42
76
  next_char = next_next_char if charinfos[next_char] && charinfos[next_char].diacritic?
43
77
  previous_char = previous_previous_char if charinfos[previous_char] && charinfos[previous_char].diacritic?
44
78
  if charinfos[previous_char] && charinfos[next_char]
45
79
  charinfos[previous_char].connects? ? :medial : :initial # If the current character does not connect,
46
- # its medial form will map to its final form,
47
- # and its initial form will map to its isolated form.
80
+ # its medial form will map to its final form,
81
+ # and its initial form will map to its isolated form.
48
82
  elsif charinfos[previous_char] # The next character is not an arabic character.
49
83
  charinfos[previous_char].connects? ? :final : :isolated
50
84
  elsif charinfos[next_char] # The previous character is not an arabic character.
@@ -54,6 +88,14 @@ module Prawn
54
88
  end
55
89
  end
56
90
 
91
+ # Transforms Arabic text by applying contextual letter forms.
92
+ #
93
+ # Processes a string character by character, determining the appropriate
94
+ # contextual form for each Arabic letter based on its surrounding characters.
95
+ # Non-Arabic characters pass through unchanged.
96
+ #
97
+ # @param str [String] the text to transform
98
+ # @return [String] the transformed text with Arabic letters in their contextual forms
57
99
  def self.transform(str)
58
100
  res = ''
59
101
  charinfos = self.charinfos
@@ -69,7 +111,8 @@ module Prawn
69
111
  next_char = next_next_char
70
112
  next_next_char = char
71
113
  return unless current_char
72
- if charinfos.keys.include?(current_char)
114
+
115
+ if charinfos.key?(current_char)
73
116
  form = determine_form(previous_previous_char, previous_char, next_char, next_next_char)
74
117
  res += charinfos[current_char].formatted[form]
75
118
  else
@@ -78,13 +121,19 @@ module Prawn
78
121
  end
79
122
  str.each_char { |char| consume_character.call(char) }
80
123
  2.times { consume_character.call(nil) }
81
- return res
124
+ res
82
125
  end
83
126
 
84
- private
85
-
127
+ # Returns the character information mapping for Arabic characters.
128
+ #
129
+ # Lazily initializes and returns a hash mapping Arabic Unicode characters
130
+ # to their CharacterInfo objects containing contextual forms.
131
+ #
132
+ # @return [Hash{String => CharacterInfo}] the character information mapping
133
+ # @api private
86
134
  def self.charinfos
87
135
  return @@charinfos unless @@charinfos.nil?
136
+
88
137
  @@charinfos = {}
89
138
  add('0627', 'fe8d', 'fe8e', 'fe8d', 'fe8e', false) # Alef
90
139
  add('0628', 'fe8f', 'fe90', 'fe91', 'fe92', true) # Ba2
@@ -135,6 +184,16 @@ module Prawn
135
184
  @@charinfos
136
185
  end
137
186
 
187
+ # Adds a character and its contextual forms to the character mapping.
188
+ #
189
+ # @param common [String] hex code of the base Unicode character
190
+ # @param isolated [String] hex code of the isolated form
191
+ # @param final [String] hex code of the final form
192
+ # @param initial [String] hex code of the initial form
193
+ # @param medial [String] hex code of the medial form
194
+ # @param connects [Boolean] whether this character connects to the next
195
+ # @param diacritic [Boolean] whether this character is a diacritic mark
196
+ # @api private
138
197
  def self.add(common, isolated, final, initial, medial, connects, diacritic = false)
139
198
  charinfo = CharacterInfo.new(
140
199
  [common.hex].pack('U'),
@@ -147,7 +206,6 @@ module Prawn
147
206
  )
148
207
  @@charinfos[charinfo.common] = charinfo
149
208
  end
150
-
151
209
  end
152
210
  end
153
211
  end
@@ -1,32 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'prawn/rtl/connector/logic'
4
- require 'twitter_cldr'
3
+ require_relative 'connector/logic'
4
+ require_relative 'bidi'
5
5
 
6
6
  module Prawn
7
7
  module Rtl
8
+ # Provides bidirectional text support and Arabic letter connection for Prawn PDF generation.
9
+ #
10
+ # This module handles RTL (Right-to-Left) text processing by:
11
+ # - Connecting Arabic script letters according to their contextual forms
12
+ # - Reordering text using the Unicode Bidirectional Algorithm
13
+ # - Supporting multiple RTL languages (Arabic, Hebrew, Persian, Urdu, etc.)
14
+ # - Handling mixed LTR/RTL text properly
15
+ #
16
+ # @example Fix Arabic text for PDF rendering
17
+ # text = "مرحبا بالعالم"
18
+ # fixed_text = Prawn::Rtl::Connector.fix_rtl(text)
19
+ #
20
+ # @example Fix Hebrew text for PDF rendering
21
+ # text = "שלום עולם"
22
+ # fixed_text = Prawn::Rtl::Connector.fix_rtl(text)
23
+ #
24
+ # @example Fix mixed LTR/RTL text
25
+ # text = "Hello مرحبا World"
26
+ # fixed_text = Prawn::Rtl::Connector.fix_rtl(text)
8
27
  module Connector
28
+ # Connects Arabic letters according to their contextual forms.
29
+ #
30
+ # @param string [String] the text containing Arabic letters to connect
31
+ # @return [String] the text with properly connected Arabic letters
9
32
  def self.connect(string)
10
33
  Prawn::Rtl::Connector::Logic.transform(string)
11
34
  end
12
35
 
36
+ # Fixes RTL text by connecting Arabic letters and reordering for visual display.
37
+ #
38
+ # This is the main entry point for processing RTL text. It detects if the text
39
+ # contains RTL characters and applies both letter connection and bidirectional
40
+ # reordering if needed.
41
+ #
42
+ # @param string [String] the text to process
43
+ # @return [String] the processed text ready for PDF rendering
13
44
  def self.fix_rtl(string)
14
45
  return string unless include_rtl?(string)
46
+
15
47
  reorder(connect(string))
16
48
  end
17
49
 
50
+ # Reorders text according to the Unicode Bidirectional Algorithm.
51
+ #
52
+ # Uses ICU's BiDi implementation via FFI to visually reorder mixed
53
+ # LTR/RTL text for correct display.
54
+ #
55
+ # @param string [String] the text to reorder
56
+ # @return [String] the visually reordered text
18
57
  def self.reorder(string)
19
- TwitterCldr::Shared::Bidi
20
- .from_string(string, direction: :RTL)
21
- .reorder_visually!
22
- .to_s
58
+ Bidi.reorder(string, direction: :rtl)
23
59
  end
24
60
 
61
+ # Checks if a string contains RTL (Right-to-Left) characters.
62
+ #
63
+ # @param string [String] the text to check
64
+ # @return [Boolean] true if the text contains RTL characters, false otherwise
25
65
  def self.include_rtl?(string)
26
- TwitterCldr::Shared::Bidi
27
- .from_string(string)
28
- .types
29
- .include?(:R)
66
+ Bidi.contains_rtl?(string)
30
67
  end
31
68
  end
32
69
  end
@@ -3,7 +3,7 @@
3
3
  module Prawn
4
4
  module Rtl
5
5
  module Support
6
- VERSION = '0.1.7'
6
+ VERSION = '0.2.0'
7
7
  end
8
8
  end
9
9
  end
@@ -1,18 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pdf/core/text'
4
- require 'prawn/rtl/support/version'
5
- require 'prawn/rtl/connector'
4
+ require_relative 'support/version'
5
+ require_relative 'connector'
6
6
 
7
7
  module Prawn
8
8
  module Rtl
9
+ # Main module for RTL support functionality.
9
10
  module Support
11
+ # Patch module that intercepts Prawn's text rendering to apply RTL transformations.
12
+ #
13
+ # This module is prepended to Prawn::Text::Formatted::Box to automatically
14
+ # process RTL text before rendering. It intercepts the original_text method
15
+ # and applies Arabic letter connection and bidirectional text reordering
16
+ # to any text fragments that contain RTL characters.
17
+ #
18
+ # @example How it works internally
19
+ # # When Prawn renders text, this patch:
20
+ # # 1. Intercepts the text fragments
21
+ # # 2. Applies RTL fixes to each fragment
22
+ # # 3. Returns the processed fragments for rendering
10
23
  module PrawnTextPatch
24
+ # Overrides the original_text method to apply RTL transformations.
25
+ #
26
+ # @return [Array<Hash>] array of text fragments with RTL text properly formatted
11
27
  def original_text
12
28
  super.map do |h|
13
- if h.key?(:text)
14
- h[:text] = Prawn::Rtl::Connector.fix_rtl(h[:text])
15
- end
29
+ h[:text] = Prawn::Rtl::Connector.fix_rtl(h[:text]) if h.key?(:text)
16
30
  h
17
31
  end
18
32
  end
@@ -1,33 +1,43 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'prawn/rtl/support/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/prawn/rtl/support/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "prawn-rtl-support"
6
+ spec.name = 'prawn-rtl-support'
8
7
  spec.version = Prawn::Rtl::Support::VERSION
9
- spec.authors = ["Oleksandr Lapchenko"]
10
- spec.email = ["ozeron@me.com"]
8
+ spec.authors = ['Oleksandr Lapchenko', 'Oleksii Leonov']
9
+ spec.email = ['ozeron@me.com', 'mail@oleksiileonov.com']
11
10
 
12
- spec.summary = 'Gem which patch prawn to provide support of arabic language.'
13
- spec.description = 'Add suport for arabic language in prawn.'
14
- spec.homepage = "https://github.com/cropio/prawn-rtl-support"
15
- spec.license = "MIT"
11
+ spec.summary = 'Bidirectional text support for Prawn PDF generator'
12
+ spec.description = 'Adds right-to-left (RTL) text support to Prawn PDF generator. ' \
13
+ 'Fully supports Arabic script languages (Arabic, Persian, Urdu) with ' \
14
+ 'contextual letter shaping and ligatures. Also supports Hebrew and other ' \
15
+ 'RTL languages with bidirectional text reordering. Handles mixed LTR/RTL text properly.'
16
+ spec.homepage = 'https://github.com/prawn-rtl-support/prawn-rtl-support'
17
+ spec.license = 'MIT'
18
+ spec.required_ruby_version = '>= 2.7.0'
16
19
 
17
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
- f.match(%r{^(test|spec|features)/})
19
- end
20
- spec.bindir = "exe"
21
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
- spec.require_paths = ["lib"]
20
+ spec.metadata = {
21
+ 'rubygems_mfa_required' => 'true',
22
+ 'source_code_uri' => 'https://github.com/prawn-rtl-support/prawn-rtl-support',
23
+ 'changelog_uri' => 'https://github.com/prawn-rtl-support/prawn-rtl-support/blob/main/CHANGELOG.md',
24
+ 'bug_tracker_uri' => 'https://github.com/prawn-rtl-support/prawn-rtl-support/issues',
25
+ 'documentation_uri' => 'https://rubydoc.info/gems/prawn-rtl-support'
26
+ }
23
27
 
24
- spec.add_development_dependency 'rake', '~> 13.0'
25
- spec.add_development_dependency 'rspec', '~> 3.9'
26
- spec.add_development_dependency 'pry', '~> 0.12'
27
- spec.add_development_dependency 'rubocop', '~> 0.77'
28
- spec.add_development_dependency 'rubocop-performance', '~> 1.5'
29
- spec.add_development_dependency 'rubocop-rspec', '~> 1.37'
28
+ # Specify which files should be included in the gem
29
+ spec.files = (
30
+ Dir['{lib,exe}/**/*'] +
31
+ Dir['*.{md,txt,gemspec}'] +
32
+ %w[Gemfile LICENSE.txt README.md CODE_OF_CONDUCT.md].select { |f| File.exist?(f) }
33
+ ).reject { |f| File.directory?(f) }
34
+
35
+ spec.bindir = 'exe'
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ['lib']
38
+ spec.extra_rdoc_files = ['README.md', 'LICENSE.txt']
30
39
 
40
+ # Runtime dependencies
41
+ spec.add_dependency 'ffi', '~> 1.17'
31
42
  spec.add_dependency 'prawn', '~> 2.2'
32
- spec.add_dependency 'twitter_cldr', '>= 4.0', '< 7.0'
33
43
  end
metadata CHANGED
@@ -1,99 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prawn-rtl-support
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleksandr Lapchenko
8
- autorequire:
8
+ - Oleksii Leonov
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-05-10 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rake
14
+ name: ffi
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '13.0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '13.0'
27
- - !ruby/object:Gem::Dependency
28
- name: rspec
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '3.9'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '3.9'
41
- - !ruby/object:Gem::Dependency
42
- name: pry
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '0.12'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '0.12'
55
- - !ruby/object:Gem::Dependency
56
- name: rubocop
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '0.77'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '0.77'
69
- - !ruby/object:Gem::Dependency
70
- name: rubocop-performance
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '1.5'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1.5'
83
- - !ruby/object:Gem::Dependency
84
- name: rubocop-rspec
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '1.37'
90
- type: :development
19
+ version: '1.17'
20
+ type: :runtime
91
21
  prerelease: false
92
22
  version_requirements: !ruby/object:Gem::Requirement
93
23
  requirements:
94
24
  - - "~>"
95
25
  - !ruby/object:Gem::Version
96
- version: '1.37'
26
+ version: '1.17'
97
27
  - !ruby/object:Gem::Dependency
98
28
  name: prawn
99
29
  requirement: !ruby/object:Gem::Requirement
@@ -108,53 +38,40 @@ dependencies:
108
38
  - - "~>"
109
39
  - !ruby/object:Gem::Version
110
40
  version: '2.2'
111
- - !ruby/object:Gem::Dependency
112
- name: twitter_cldr
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '4.0'
118
- - - "<"
119
- - !ruby/object:Gem::Version
120
- version: '7.0'
121
- type: :runtime
122
- prerelease: false
123
- version_requirements: !ruby/object:Gem::Requirement
124
- requirements:
125
- - - ">="
126
- - !ruby/object:Gem::Version
127
- version: '4.0'
128
- - - "<"
129
- - !ruby/object:Gem::Version
130
- version: '7.0'
131
- description: Add suport for arabic language in prawn.
41
+ description: Adds right-to-left (RTL) text support to Prawn PDF generator. Fully supports
42
+ Arabic script languages (Arabic, Persian, Urdu) with contextual letter shaping and
43
+ ligatures. Also supports Hebrew and other RTL languages with bidirectional text
44
+ reordering. Handles mixed LTR/RTL text properly.
132
45
  email:
133
46
  - ozeron@me.com
47
+ - mail@oleksiileonov.com
134
48
  executables: []
135
49
  extensions: []
136
- extra_rdoc_files: []
50
+ extra_rdoc_files:
51
+ - LICENSE.txt
52
+ - README.md
137
53
  files:
138
- - ".gitignore"
139
- - ".rspec"
140
- - ".travis.yml"
54
+ - CHANGELOG.md
55
+ - CLAUDE.md
141
56
  - CODE_OF_CONDUCT.md
142
57
  - Gemfile
143
58
  - LICENSE.txt
144
59
  - README.md
145
- - Rakefile
146
- - bin/console
147
- - bin/setup
60
+ - lib/prawn/rtl/bidi.rb
148
61
  - lib/prawn/rtl/connector.rb
149
62
  - lib/prawn/rtl/connector/logic.rb
150
63
  - lib/prawn/rtl/support.rb
151
64
  - lib/prawn/rtl/support/version.rb
152
65
  - prawn-rtl-support.gemspec
153
- homepage: https://github.com/cropio/prawn-rtl-support
66
+ homepage: https://github.com/prawn-rtl-support/prawn-rtl-support
154
67
  licenses:
155
68
  - MIT
156
- metadata: {}
157
- post_install_message:
69
+ metadata:
70
+ rubygems_mfa_required: 'true'
71
+ source_code_uri: https://github.com/prawn-rtl-support/prawn-rtl-support
72
+ changelog_uri: https://github.com/prawn-rtl-support/prawn-rtl-support/blob/main/CHANGELOG.md
73
+ bug_tracker_uri: https://github.com/prawn-rtl-support/prawn-rtl-support/issues
74
+ documentation_uri: https://rubydoc.info/gems/prawn-rtl-support
158
75
  rdoc_options: []
159
76
  require_paths:
160
77
  - lib
@@ -162,15 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
162
79
  requirements:
163
80
  - - ">="
164
81
  - !ruby/object:Gem::Version
165
- version: '0'
82
+ version: 2.7.0
166
83
  required_rubygems_version: !ruby/object:Gem::Requirement
167
84
  requirements:
168
85
  - - ">="
169
86
  - !ruby/object:Gem::Version
170
87
  version: '0'
171
88
  requirements: []
172
- rubygems_version: 3.0.3
173
- signing_key:
89
+ rubygems_version: 3.6.7
174
90
  specification_version: 4
175
- summary: Gem which patch prawn to provide support of arabic language.
91
+ summary: Bidirectional text support for Prawn PDF generator
176
92
  test_files: []
data/.gitignore DELETED
@@ -1,14 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
-
11
- # rspec failure tracking
12
- .rspec_status
13
-
14
- *.gem
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --format documentation
2
- --color
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.6
4
- - 2.5
5
- - 2.4
6
- before_install:
7
- - gem install bundler
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "prawn/rtl/support"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "pry"
14
- Pry.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here