├── .gitignore ├── lib ├── unicopy │ ├── version.rb │ └── kernel_method.rb └── unicopy.rb ├── Gemfile ├── CHANGELOG.md ├── MIT-LICENSE.txt ├── Rakefile ├── unicopy.gemspec ├── .github └── workflows │ └── test.yml ├── CODE_OF_CONDUCT.md ├── spec └── unicopy_spec.rb ├── README.md └── bin └── unicopy /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | /pkg 3 | -------------------------------------------------------------------------------- /lib/unicopy/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Unicopy 4 | VERSION = "1.0.3" 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'minitest' 6 | gem 'rake' 7 | gem 'irbtools', require: 'irbtools/binding' unless RUBY_ENGINE == "jruby" 8 | -------------------------------------------------------------------------------- /lib/unicopy/kernel_method.rb: -------------------------------------------------------------------------------- 1 | require_relative '../unicopy' 2 | 3 | module Kernel 4 | private 5 | 6 | def unicopy(string, **kwargs) 7 | Unicopy.unicopy(string, **kwargs) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | ### 1.0.3 4 | 5 | * Allow clipboard gem v2 6 | 7 | ### 1.0.2 8 | 9 | * Re-release with MFA setting in gemspec + updated CI 10 | 11 | ### 1.0.1 12 | 13 | * Relax Ruby version requirement to allow Ruby 3.0 14 | 15 | ### 1.0.0 16 | 17 | * Initial release 18 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2021 Jan Lelis, https://janlelis.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # # # 2 | # Get gemspec info 3 | 4 | gemspec_file = Dir['*.gemspec'].first 5 | gemspec = eval File.read(gemspec_file), binding, gemspec_file 6 | info = "#{gemspec.name} | #{gemspec.version} | " \ 7 | "#{gemspec.runtime_dependencies.size} dependencies | " \ 8 | "#{gemspec.files.size} files" 9 | 10 | # # # 11 | # Gem build and install task 12 | 13 | desc info 14 | task :gem do 15 | puts info + "\n\n" 16 | print " "; sh "gem build #{gemspec_file}" 17 | FileUtils.mkdir_p 'pkg' 18 | FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg' 19 | puts; sh %{gem install --no-document pkg/#{gemspec.name}-#{gemspec.version}.gem} 20 | end 21 | 22 | # # # 23 | # Start an IRB session with the gem loaded 24 | 25 | desc "#{gemspec.name} | IRB" 26 | task :irb do 27 | sh "irb -I ./lib -r #{gemspec.name.gsub '-','/'}/kernel_method" 28 | end 29 | 30 | # # # 31 | # Run specs 32 | 33 | desc "#{gemspec.name} | Spec" 34 | task :spec do 35 | if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ 36 | sh "for %f in (spec/\*.rb) do ruby spec/%f" 37 | else 38 | sh "for file in spec/*.rb; do ruby $file; done" 39 | end 40 | end 41 | task default: :spec 42 | 43 | -------------------------------------------------------------------------------- /unicopy.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.dirname(__FILE__) + "/lib/unicopy/version" 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "unicopy" 7 | gem.version = Unicopy::VERSION 8 | gem.summary = "Converts Unicode codepoints to a string (or vice versa) and copies it to the system clipboard" 9 | gem.description = "Converts Unicode codepoints to a string (or vice versa) and copies it to the system clipboard. Can also convert codepoints to many dump formats." 10 | gem.authors = ["Jan Lelis"] 11 | gem.email = ["hi@ruby.consulting"] 12 | gem.homepage = "https://github.com/janlelis/unicopy" 13 | gem.license = "MIT" 14 | 15 | gem.files = Dir["{**/}{.*,*}"].select{ |path| File.file?(path) && path !~ /^(pkg|screenshots)/ } 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | gem.metadata = { "rubygems_mfa_required" => "true" } 20 | 21 | gem.add_dependency 'paint', '>= 0.9', '< 3.0' 22 | gem.add_dependency 'rationalist', '~> 2.0' 23 | gem.add_dependency 'clipboard', '>= 1.1', '< 3.0' 24 | 25 | gem.required_ruby_version = ">= 2.0" 26 | end 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Ruby ${{ matrix.ruby }} (${{ matrix.os }}) 8 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 9 | strategy: 10 | matrix: 11 | ruby: 12 | - '3.3' 13 | - '3.2' 14 | - '3.1' 15 | - '3.0' 16 | - jruby 17 | - truffleruby 18 | os: 19 | - ubuntu-latest 20 | - macos-latest 21 | runs-on: ${{matrix.os}} 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Ruby 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{matrix.ruby}} 28 | bundler-cache: true 29 | - name: Run tests 30 | run: bundle exec rake 31 | 32 | test-windows: 33 | name: Ruby ${{ matrix.ruby }} (windows-latest) 34 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 35 | strategy: 36 | matrix: 37 | ruby: 38 | - '3.3' 39 | - '3.2' 40 | - '3.1' 41 | - '3.0' 42 | - jruby 43 | runs-on: windows-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Set up Ruby 47 | uses: ruby/setup-ruby@v1 48 | with: 49 | ruby-version: ${{matrix.ruby}} 50 | bundler-cache: true 51 | - name: Run tests 52 | run: bundle exec rake 53 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@janlelis.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /spec/unicopy_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/unicopy/kernel_method" 2 | require "minitest/autorun" 3 | 4 | describe Unicopy do 5 | it "converts codepoints to string and, with --print, outputs it to STDOUT" do 6 | assert_output(/Ruby 🌫/){ unicopy("52 75 62 79 20 1F32B", print: true) } 7 | end 8 | 9 | it "also works with U+ prefixes" do 10 | assert_output(/Ruby 🌫/){ unicopy("U+0052 U+0075 U+0062 U+0079 U+0020 U+1F32B", print: true) } 11 | end 12 | 13 | it "parses codepoints as decimal if --parse-decimal option is given" do 14 | assert_output(/4K>O/){ unicopy("52 75 62 79", print: true, parse_decimal: true) } 15 | end 16 | 17 | it "will convert string to codepoints with --string option" do 18 | assert_output(Regexp.compile(Regexp.escape("U+0052 U+0075 U+0062 U+0079 U+0020 U+1F32B"))){ 19 | unicopy("Ruby 🌫", print: true, string: true) 20 | } 21 | end 22 | 23 | it "works with non-UTF-8 input" do 24 | assert_output(Regexp.compile(Regexp.escape("U+0052 U+0075 U+0062 U+0079 U+0020 U+1F32B"))){ 25 | unicopy("Ruby 🌫".encode("UTF-16LE"), print: true, string: true) 26 | } 27 | end 28 | 29 | describe "[dump formats]" do 30 | it "dumps hex" do 31 | assert_output(Regexp.compile(Regexp.escape("52 75 62 79 20 1F32B"))){ 32 | unicopy("52 75 62 79 20 1F32B", print: true, dump: "hex") 33 | } 34 | end 35 | 36 | it "dumps hex (string input)" do 37 | assert_output(Regexp.compile(Regexp.escape("52 75 62 79 20 1F32B"))){ 38 | unicopy("Ruby 🌫", print: true, string: true, dump: "hex") 39 | } 40 | end 41 | 42 | it "dumps uplus (string input) [default]" do 43 | assert_output(Regexp.compile(Regexp.escape("U+0052 U+0075 U+0062 U+0079 U+0020 U+1F32B"))){ 44 | unicopy("Ruby 🌫", print: true, string: true) 45 | } 46 | end 47 | 48 | it "dumps 0x (string input)" do 49 | assert_output(Regexp.compile(Regexp.escape("0x0052 0x0075 0x0062 0x0079 0x0020 0x1F32B"))){ 50 | unicopy("Ruby 🌫", print: true, string: true, dump: "0x") 51 | } 52 | end 53 | 54 | it "dumps dec (string input)" do 55 | assert_output(Regexp.compile(Regexp.escape("82 117 98 121 32 127787"))){ 56 | unicopy("Ruby 🌫", print: true, string: true, dump: "dec") 57 | } 58 | end 59 | 60 | it "dumps ruby (string input)" do 61 | assert_output(Regexp.compile(Regexp.escape("\\u{52 75 62 79 20 1F32B}"))){ 62 | unicopy("Ruby 🌫", print: true, string: true, dump: "ruby") 63 | } 64 | end 65 | 66 | it "dumps js (string input)" do 67 | assert_output(Regexp.compile(Regexp.escape("\\u0052\\u0075\\u0062\\u0079\\u0020\\uD83C\\uDF2B"))){ 68 | unicopy("Ruby 🌫", print: true, string: true, dump: "js") 69 | } 70 | end 71 | 72 | it "dumps es6 (string input)" do 73 | assert_output(Regexp.compile(Regexp.escape("\\u{52}\\u{75}\\u{62}\\u{79}\\u{20}\\u{1F32B}"))){ 74 | unicopy("Ruby 🌫", print: true, string: true, dump: "es6") 75 | } 76 | end 77 | 78 | it "dumps css (string input)" do 79 | assert_output(Regexp.compile(Regexp.escape("\\52\\75\\62\\79\\20\\1f32b"))){ 80 | unicopy("Ruby 🌫", print: true, string: true, dump: "css") 81 | } 82 | end 83 | 84 | it "dumps html-hex (string input)" do 85 | assert_output(Regexp.compile(Regexp.escape("Ruby 🌫"))){ 86 | unicopy("Ruby 🌫", print: true, string: true, dump: "html-hex") 87 | } 88 | end 89 | 90 | it "dumps html-dec (string input)" do 91 | assert_output(Regexp.compile(Regexp.escape("Ruby 🌫"))){ 92 | unicopy("Ruby 🌫", print: true, string: true, dump: "html-dec") 93 | } 94 | end 95 | 96 | it "dumps bytes-utf8 (string input)" do 97 | assert_output(Regexp.compile(Regexp.escape("52 75 62 79 20 F0 9F 8C AB"))){ 98 | unicopy("Ruby 🌫", print: true, string: true, dump: "bytes-utf8") 99 | } 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unicopy | Convert the Codepoints [![[version]](https://badge.fury.io/rb/unicopy.svg)](https://badge.fury.io/rb/unicopy) [![[ci]](https://github.com/janlelis/unicopy/workflows/Test/badge.svg)](https://github.com/janlelis/unicopy/actions?query=workflow%3ATest) 2 | 3 | CLI utility which converts Unicode codepoints to a string (or vice versa). Copies the result to the system clipboard or just prints it to the console. 4 | 5 | Can also convert codepoints to many dump formats. 6 | 7 | ## Setup 8 | 9 | Make sure you have Ruby installed and installing gems works properly. Then do: 10 | 11 | ``` 12 | $ gem install unicopy 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Codepoints to String 18 | 19 | Codepoints format is hexadecimal, "U+" is permitted, examples: 20 | 21 | ```ruby 22 | $ unicopy 52 75 62 79 --print 23 | Ruby 24 | ``` 25 | 26 | ```ruby 27 | $ unicopy U+0052 U+0075 U+0062 U+0079 --print 28 | Ruby 29 | ``` 30 | 31 | ### String to Codepoints 32 | 33 | ```ruby 34 | $ unicopy --string Ruby --print 35 | U+0052 U+0075 U+0062 U+0079 36 | ``` 37 | 38 | ### Options 39 | 40 | ``` 41 | --help | | this help page 42 | --parse-decimal | | interpret given codepoints as decimal, instead of hexadecimal values 43 | --print | -p | do not copy to system clipboard, but print to screen 44 | --string | -s | see above 45 | --version | | displays version of unicopy 46 | --(dump format) | | specify the format to be used for dumping the codepoints (see below) 47 | ``` 48 | 49 | ### Dump Formats 50 | 51 | #### Short Hexadecimal 52 | 53 | ``` 54 | $ unicopy 52 75 62 79 20 1F32B --print --hex 55 | 52 75 62 79 20 1F32B 56 | ``` 57 | 58 | ``` 59 | $ unicopy --string "Ruby 🌫" --print --hex 60 | 52 75 62 79 20 1F32B 61 | ``` 62 | 63 | #### Hexadecimal With `U+` Prefix 64 | 65 | ``` 66 | $ unicopy 52 75 62 79 20 1F32B --print --uplus 67 | U+0052 U+0075 U+0062 U+0079 U+0020 U+1F32B 68 | ``` 69 | 70 | ``` 71 | $ unicopy --string "Ruby 🌫" --print --uplus 72 | U+0052 U+0075 U+0062 U+0079 U+0020 U+1F32B 73 | ``` 74 | 75 | #### Hexadecimal With `0x` Prefix 76 | 77 | ``` 78 | $ unicopy 52 75 62 79 20 1F32B --print --0x 79 | 0x0052 0x0075 0x0062 0x0079 0x0020 0x1F32B 80 | ``` 81 | 82 | ``` 83 | $ unicopy --string "Ruby 🌫" --print --0x 84 | 0x0052 0x0075 0x0062 0x0079 0x0020 0x1F32B 85 | ``` 86 | 87 | #### Decimal 88 | 89 | ``` 90 | $ unicopy 52 75 62 79 20 1F32B --print --dec 91 | 82 117 98 121 32 127787 92 | ``` 93 | 94 | ``` 95 | $ unicopy --string "Ruby 🌫" --print --dec 96 | 82 117 98 121 32 127787 97 | ``` 98 | 99 | #### Ruby Escape Syntax 100 | 101 | ``` 102 | $ unicopy 52 75 62 79 20 1F32B --print --ruby 103 | \\u{52 75 62 79 20 1F32B} 104 | ``` 105 | 106 | ``` 107 | $ unicopy --string "Ruby 🌫" --print --ruby 108 | \\u{52 75 62 79 20 1F32B} 109 | ``` 110 | 111 | #### JavaScript Escape Syntax (Since ES6) 112 | 113 | ``` 114 | $ unicopy 52 75 62 79 20 1F32B --print --es6 115 | \\u{52}\\u{75}\\u{62}\\u{79}\\u{20}\\u{1F32B} 116 | ``` 117 | 118 | ``` 119 | $ unicopy --string "Ruby 🌫" --print --es6 120 | \\u{52}\\u{75}\\u{62}\\u{79}\\u{20}\\u{1F32B} 121 | ``` 122 | 123 | #### JavaScript Escape Syntax (Before ES6) 124 | 125 | ``` 126 | $ unicopy 52 75 62 79 20 1F32B --print --js 127 | \\u0052\\u0075\\u0062\\u0079\\u0020\\uD83C\\uDF2B 128 | ``` 129 | 130 | ``` 131 | $ unicopy --string "Ruby 🌫" --print --js 132 | \\u0052\\u0075\\u0062\\u0079\\u0020\\uD83C\\uDF2B 133 | ``` 134 | 135 | #### CSS Escape Syntax 136 | 137 | ``` 138 | $ unicopy 52 75 62 79 20 1F32B --print --css 139 | \\52\\75\\62\\79\\20\\1f32b 140 | ``` 141 | 142 | ``` 143 | $ unicopy --string "Ruby 🌫" --print --css 144 | \\52\\75\\62\\79\\20\\1f32b 145 | ``` 146 | 147 | #### Hexadecimal HTML Entities 148 | 149 | ``` 150 | $ unicopy 52 75 62 79 20 1F32B --print --html-hex 151 | Ruby 🌫 152 | ``` 153 | 154 | ``` 155 | $ unicopy --string "Ruby 🌫" --print --html-hex 156 | Ruby 🌫 157 | ``` 158 | 159 | #### Decimal HTML Entities 160 | 161 | ``` 162 | $ unicopy 52 75 62 79 20 1F32B --print --html-dec 163 | Ruby 🌫 164 | ``` 165 | 166 | ``` 167 | $ unicopy --string "Ruby 🌫" --print --html-dec 168 | Ruby 🌫 169 | ``` 170 | 171 | #### UTF-8 in Hexadecimal Bytes 172 | 173 | ``` 174 | $ unicopy 52 75 62 79 20 1F32B --print --bytes-utf8 175 | 52 75 62 79 20 F0 9F 8C AB 176 | ``` 177 | 178 | ``` 179 | $ unicopy --string "Ruby 🌫" --print --bytes-utf8 180 | 52 75 62 79 20 F0 9F 8C AB 181 | ``` 182 | 183 | ## Also see 184 | 185 | - [unibits](https://github.com/janlelis/unibits) 186 | - [uniscribe](https://github.com/janlelis/uniscribe) 187 | 188 | Copyright (C) 2017-2021 Jan Lelis . Released under the MIT license. 189 | -------------------------------------------------------------------------------- /lib/unicopy.rb: -------------------------------------------------------------------------------- 1 | # enable-frozen-string: true 2 | 3 | require_relative "unicopy/version" 4 | 5 | require "clipboard" 6 | 7 | module Unicopy 8 | DUMP_FORMATS = { 9 | 'hex' => { 10 | description: "____ (hexadecimal)", 11 | replace: "%X", 12 | alias: "x", 13 | }, 14 | 'uplus' => { 15 | description: "U+____ (hexadecimal)", 16 | replace: "U+%04X", 17 | alias: "u", 18 | }, 19 | '0x' => { 20 | description: "0x____ (hexadecimal)", 21 | replace: "0x%04X", 22 | alias: "0", 23 | }, 24 | 'dec' => { 25 | description: "____ (decimal)", 26 | replace: "%d", 27 | alias: "d", 28 | }, 29 | 'ruby' => { 30 | description: "Ruby \\u{____} (hexadecimal)", 31 | prefix: "\\u{", 32 | replace: "%X", 33 | suffix: "}", 34 | alias: "r", 35 | }, 36 | 'css' => { 37 | description: "CSS \\____ (hexadecimal)", 38 | replace: "\\%x", 39 | joiner: "", 40 | suffix: " ", 41 | }, 42 | 'js' => { 43 | description: "JavaScript \\u____ with UTF-16 surrogates (hexadecimal)", 44 | replace: ->(x){ 45 | [x].pack("U").encode("UTF-16BE").each_byte.each_slice(2).map{ |a,b| "\\u%04X" % (a*256 + b) } 46 | }, 47 | joiner: "", 48 | }, 49 | 'es6' => { 50 | description: "EcmaScript 6+ \\u{____} (hexadecimal)", 51 | replace: "\\u{%X}", 52 | joiner: "", 53 | alias: "6", 54 | }, 55 | 'html-hex' => { 56 | description: "HTML entitiy &#x____ (hexadecimal)", 57 | replace: "&#x%X;", 58 | joiner: "", 59 | }, 60 | 'html-dec' => { 61 | description: "HTML entitiy &#____ (decimal)", 62 | replace: "&#%d;", 63 | joiner: "", 64 | }, 65 | 'bytes-utf8' => { 66 | description: "UTF-8 encoded (bytes)", 67 | replace: ->(x){ 68 | [x].pack("U").unpack("C*").map{ |x| x.to_s(16).upcase }.join(" ") 69 | }, 70 | } 71 | } 72 | DUMP_FORMAT_DEFAULTS = { 73 | prefix: "", 74 | replace: "%s", 75 | joiner: " ", 76 | suffix: "", 77 | } 78 | 79 | def self.unicopy(*args, string: false, dump: nil, parse_decimal: false, print: false) 80 | if args.empty? 81 | raise ArgumentError, "no data given to copy" 82 | elsif !string 83 | codepoints = transform_raw_codepoints(args.join(" ").split(" "), parse_decimal) 84 | if dump 85 | deliver(dump_codepoints(codepoints, format: dump), print: print, codepoints_length: codepoints.length, message: "dump of") 86 | else # default 87 | deliver(codepoints.pack("U*"), print: print, codepoints_length: codepoints.length, message: "string of") 88 | end 89 | else 90 | data = args.join("") 91 | data = data.encode("UTF-8") unless data.encoding.name == "UTF-8" 92 | codepoints = data.unpack("U*") 93 | 94 | deliver(dump_codepoints(codepoints, format: dump), print: print, codepoints_length: codepoints.length, message: "dump of") 95 | end 96 | end 97 | 98 | def self.deliver(output, print: false, message: "", codepoints_length:) 99 | if print 100 | puts(output) 101 | else 102 | Clipboard.copy(output) 103 | puts Paint["Copied #{message} #{codepoints_length} codepoint#{codepoints_length != 1 ? "s" : ""} to clipboard", :green] 104 | end 105 | end 106 | 107 | def self.transform_raw_codepoints(codepoints, parse_decimal = false) 108 | if parse_decimal 109 | parse_numerals_regex = /\A([0-9]+)\z/ 110 | numeral_base = 10 111 | else 112 | parse_numerals_regex = /\A(?:U\+)?(\h+)\z/ 113 | numeral_base = 16 114 | end 115 | 116 | codepoints.map{ |cp| 117 | case cp 118 | when Integer 119 | cp 120 | when parse_numerals_regex 121 | $1.to_i(numeral_base) 122 | else 123 | raise ArgumentError, "unicopy does not understand codepoint \"#{cp}\". Please pass hexadecimal codepoint values, separated by spaces, or use --string option" 124 | end 125 | }.tap{ |codepoints| 126 | codepoints.pack("U*").valid_encoding? or raise ArgumentError, "invalid codepoints passed to unicopy" 127 | } 128 | end 129 | 130 | def self.dump_codepoints(codepoints, format: nil) 131 | format ||= "uplus" 132 | if dump_format_details = DUMP_FORMATS[format] 133 | prefix = dump_format_details[:prefix] || DUMP_FORMAT_DEFAULTS[:prefix] 134 | replace = dump_format_details[:replace] || DUMP_FORMAT_DEFAULTS[:replace] 135 | joiner = dump_format_details[:joiner] || DUMP_FORMAT_DEFAULTS[:joiner] 136 | suffix = dump_format_details[:suffix] || DUMP_FORMAT_DEFAULTS[:suffix] 137 | else 138 | raise ArgumentError, "unicopy does not know dump format \"#{format}\"" 139 | end 140 | 141 | prefix + codepoints.map{ |cp| replace.is_a?(Proc) ? replace[cp] : replace % cp }.join(joiner) + suffix 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /bin/unicopy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rationalist" 4 | require "paint" 5 | require "unicopy" 6 | 7 | argv = Rationalist.parse( 8 | ARGV, 9 | string: '_', 10 | alias: { 11 | p: 'print', 12 | s: 'string', 13 | v: 'version', 14 | }.merge(Hash[Unicopy::DUMP_FORMATS.select{ |k,v| v[:alias] }.map{ |k,v| [v[:alias], k] }]), 15 | boolean: [ 16 | 'parse-decimal', 17 | 'help', 18 | 'print', 19 | 'string', 20 | 'version', 21 | *Unicopy::DUMP_FORMATS.keys 22 | ] 23 | ) 24 | 25 | if argv[:version] 26 | puts "unicopy #{Unicopy::VERSION} by #{Paint["J-_-L", :bold]} " 27 | exit(0) 28 | end 29 | 30 | 31 | if argv[:help] 32 | dump_format_description = Unicopy::DUMP_FORMATS.map{ |dump_format, info| 33 | "--#{dump_format.ljust(20)}| #{info[:alias] ? "-#{info[:alias]}" : " "} | #{info[:description]}" 34 | }.join("\n ") 35 | 36 | puts <<-HELP 37 | 38 | #{Paint["DESCRIPTION", :underline]} 39 | 40 | Converts Unicode codepoints to a string (or vice versa) and copies it to the system clipboard. 41 | Can also convert codepoints to many dump formats. 42 | 43 | #{Paint["USAGE", :underline]} 44 | 45 | CODEPOINTS TO STRING 46 | 47 | #{Paint["unicopy", :bold]} [options] codepoints 48 | 49 | Codepoints format is hexadecimal, "U+" is permitted, examples: 50 | 51 | $ unicopy 52 75 62 79 20 1F32B --print 52 | Ruby 🌫 53 | 54 | $ unicopy U+0052 U+0075 U+0062 U+0079 U+0020 U+1F32B --print 55 | Ruby 🌫 56 | 57 | STRING TO CODEPOINTS 58 | 59 | #{Paint["unicopy", :bold]} (--string|-s) [options] string 60 | 61 | $ unicopy -sp "Ruby 🌫" 62 | U+0052 U+0075 U+0062 U+0079 U+0020 U+1F32B 63 | 64 | If data is retrieved via STDIN, it will split alongside newlines. 65 | 66 | #{Paint["OPTIONS", :underline]} 67 | 68 | --help | | this help page 69 | --parse-decimal | | interpret given codepoints as decimal, instead of hexadecimal values 70 | --print | -p | do not copy to system clipboard, but print to screen 71 | --string | -s | see above 72 | --version | | displays version of unicopy 73 | --(dump format) | | specify the format to be used for dumping the codepoints (see below) 74 | 75 | #{Paint["DUMP FORMATS", :underline]} 76 | 77 | #{dump_format_description} 78 | 79 | Examples: 80 | 81 | $ unicopy 52 75 62 79 20 1F32B --print --hex 82 | 52 75 62 79 20 1F32B 83 | 84 | $ unicopy --string "Ruby 🌫" --print --hex 85 | 52 75 62 79 20 1F32B 86 | 87 | $ unicopy 52 75 62 79 20 1F32B --print --uplus 88 | U+0052 U+0075 U+0062 U+0079 U+0020 U+1F32B 89 | 90 | $ unicopy --string "Ruby 🌫" --print --uplus 91 | U+0052 U+0075 U+0062 U+0079 U+0020 U+1F32B 92 | 93 | $ unicopy 52 75 62 79 20 1F32B --print --0x 94 | 0x0052 0x0075 0x0062 0x0079 0x0020 0x1F32B 95 | 96 | $ unicopy --string "Ruby 🌫" --print --0x 97 | 0x0052 0x0075 0x0062 0x0079 0x0020 0x1F32B 98 | 99 | $ unicopy 52 75 62 79 20 1F32B --print --dec 100 | 82 117 98 121 32 127787 101 | 102 | $ unicopy --string "Ruby 🌫" --print --dec 103 | 82 117 98 121 32 127787 104 | 105 | $ unicopy 52 75 62 79 20 1F32B --print --ruby 106 | \\u{52 75 62 79 20 1F32B} 107 | 108 | $ unicopy --string "Ruby 🌫" --print --ruby 109 | \\u{52 75 62 79 20 1F32B} 110 | 111 | $ unicopy 52 75 62 79 20 1F32B --print --js 112 | \\u0052\\u0075\\u0062\\u0079\\u0020\\uD83C\\uDF2B 113 | 114 | $ unicopy --string "Ruby 🌫" --print --js 115 | \\u0052\\u0075\\u0062\\u0079\\u0020\\uD83C\\uDF2B 116 | 117 | $ unicopy 52 75 62 79 20 1F32B --print --es6 118 | \\u{52}\\u{75}\\u{62}\\u{79}\\u{20}\\u{1F32B} 119 | 120 | $ unicopy --string "Ruby 🌫" --print --es6 121 | \\u{52}\\u{75}\\u{62}\\u{79}\\u{20}\\u{1F32B} 122 | 123 | $ unicopy 52 75 62 79 20 1F32B --print --css 124 | \\52\\75\\62\\79\\20\\1f32b 125 | 126 | $ unicopy --string "Ruby 🌫" --print --css 127 | \\52\\75\\62\\79\\20\\1f32b 128 | 129 | $ unicopy 52 75 62 79 20 1F32B --print --html-hex 130 | Ruby 🌫 131 | 132 | $ unicopy --string "Ruby 🌫" --print --html-hex 133 | Ruby 🌫 134 | 135 | $ unicopy 52 75 62 79 20 1F32B --print --html-dec 136 | Ruby 🌫 137 | 138 | $ unicopy --string "Ruby 🌫" --print --html-dec 139 | Ruby 🌫 140 | 141 | $ unicopy 52 75 62 79 20 1F32B --print --bytes-utf8 142 | 52 75 62 79 20 F0 9F 8C AB 143 | 144 | $ unicopy --string "Ruby 🌫" --print --bytes-utf8 145 | 52 75 62 79 20 F0 9F 8C AB 146 | 147 | #{Paint["MORE INFO", :underline]} 148 | 149 | https://github.com/janlelis/unicopy 150 | 151 | HELP 152 | exit(0) 153 | end 154 | 155 | if argv[:_] 156 | codepoints = argv.delete :_ 157 | elsif !$stdin.tty? 158 | codepoints = $stdin.read.split($/) 159 | else 160 | codepoints = nil 161 | end 162 | 163 | options = { 164 | parse_decimal: argv[:"parse-decimal"], 165 | print: argv[:print], 166 | string: argv[:string], 167 | } 168 | 169 | dump_formats = Unicopy::DUMP_FORMATS.keys.select{ |dump_format| argv[dump_format.to_sym] } 170 | case dump_formats.size 171 | when 0 172 | # nothing 173 | when 1 174 | options[:dump] = dump_formats[0] 175 | else 176 | raise ArgumentError, "only one dump format can be passed to unicopy" 177 | end 178 | 179 | begin 180 | Unicopy.unicopy(*codepoints, **options) 181 | rescue ArgumentError 182 | $stderr.puts Paint[$!.message, :red] 183 | exit(1) 184 | end 185 | --------------------------------------------------------------------------------