amagi-captions 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6e6e70d742c9b9f90fb4f494641353e883d9f854a15e50d20e8cc1a0e85295dd
4
+ data.tar.gz: 6fde8355de7aa80ffc716923fac935ae8338a66beb42df0a76b5a4c09f6afbe6
5
+ SHA512:
6
+ metadata.gz: 9a6c0bfc0c3466f0ae922cc46144018e552a7cbd2fddc6c35de807368951eef6fc8a3bb3f2166852dcb8fa26063bc56bf29f87629f28423e4bc1404d2fca7789
7
+ data.tar.gz: 3001b3ce3620f766a2b7698b403747d290c604323119daeb32086eef80453b4104db59ab9ae56fd793f4ad95850f5f41c83652709837910985a3774200dec2b9
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ cache: bundler
3
+
4
+ rvm:
5
+ - ruby-2.3.8
6
+
7
+ script: 'bundle exec rspec'
8
+
9
+ notifications:
10
+ email:
11
+ recipients:
12
+ - navinre93@gmail.com
13
+ on_failure: change
14
+ on_success: never
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in captions.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 navin
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # Captions - Subtitle Editor and Converter
2
+
3
+ [![Build Status](https://travis-ci.org/navinre/captions.svg?branch=master)](https://travis-ci.org/navinre/captions)
4
+
5
+ Captions can read subtitles of various formats, modify them and convert the subtitle to other formats. Currently captions supports `SRT`, `WebVTT` files.
6
+
7
+ ## Supported Features
8
+ - Read subtitles
9
+ - Move subtitles
10
+ - Increase duration
11
+ - Filter subtitles
12
+ - Change frame rate
13
+ - Convert to other format (SRT, WebVTT)
14
+
15
+ ## Features Planned
16
+ - Read subtitle properties (Font Style, Aligment, Position etc.)
17
+ - Delete subtitles
18
+ - Change subtitle fonts
19
+ - Command Line Interface
20
+ - Spell Check
21
+ - And More...
22
+
23
+
24
+ ## Usage
25
+
26
+ **Read subtitle file:**
27
+
28
+ ```ruby
29
+ s = Captions::SRT.new('test.srt')
30
+ s.parse
31
+
32
+ s = Captions::VTT.new('test.vtt')
33
+ s.parse
34
+ ```
35
+
36
+ **Filter subtitles:**
37
+
38
+ Once the subtitle file has been parsed. Every subtitle will get **start_time, end_time, duration and text**. All the values are stored in milliseconds. `Captions::InvalidSubtitle` error will be thown if start-time or end-time cannot be found for a subtitle.
39
+
40
+ View all subtitle text:
41
+ ```ruby
42
+ s.cues.map { |c| c.text }
43
+ ```
44
+
45
+ Filter subtitles based on condition:
46
+ ```ruby
47
+ s.cues { |c| c.start_time > 1000 }
48
+ s.cues { |c| c.end_time > 1000 }
49
+ s.cues { |c| c.duration > 1000 }
50
+ ```
51
+
52
+ To get all subtitles:
53
+ ```ruby
54
+ s.cues.each { |c| puts c }
55
+ ```
56
+
57
+ **Move Subtitle:**
58
+
59
+ ```ruby
60
+ s.move_by(1000)
61
+ s.move_by(1000) { |c| c.start_time > 3000 }
62
+ ```
63
+
64
+ Former command moves all subtitles by 1 second. Later moves the subtitles which are starting after 3 seconds by 1 second.
65
+
66
+ **Increase Duration:**
67
+
68
+ ```ruby
69
+ s.increase_duration_by(1000)
70
+ s.increase_duration_by(1000) { |c| c.start_time > 3000 }
71
+ ```
72
+
73
+ Former command increases duration of all subtitles by 1 second. Later increases the duration of subtitles which are starting after 3 seconds by 1 second.
74
+
75
+ **Change Frame Rate:**
76
+
77
+ All subtitles are parsed based on **25 frames/second** by default. This can be changed by passing frame rate at the time of reading the subtitle file.
78
+
79
+ ```ruby
80
+ s = Captions::SRT.new('test.srt', 29.97)
81
+ s.parse
82
+ ```
83
+
84
+ Frame rate can also be changed after parsing. This command changes all the subtitles which are parsed in different frame-rate to new frame-rate.
85
+
86
+ ```ruby
87
+ s.set_frame_rate(29.97)
88
+ ```
89
+
90
+ **Convert to Other Format:**
91
+
92
+ Subtitles parsed in one format can be converted to other format. Currently export is supported for **SRT** and **WebVTT**. Other formats will be added soon.
93
+
94
+ ```ruby
95
+ s = Captions::SRT.new('test.srt')
96
+ s.parse
97
+ s.export_to_vtt('test.vtt')
98
+ ```
99
+
100
+ Subtitles can also be filtered and those filtered subtitles can be written to a file.
101
+
102
+ ```ruby
103
+ s = Captions::SRT.new('test.srt')
104
+ s.parse
105
+ new_cues = s.cues { |c| c.start_time > 2000 }
106
+ s.export_to_vtt('test.vtt', new_cues)
107
+ ```
108
+
109
+ **Filter subtiltes and Export:**
110
+
111
+ Subtitles can be filtered with filter option. This returns a new `Captions` object. Once filters new object is created and cues for that becomes the result of the filter. This can be exported to other formats easily.
112
+
113
+ ```ruby
114
+ new_obj = s.filter { |c| c.start_time > 2000 }
115
+ new_obj.export_to_vtt('test.vtt')
116
+ ```
117
+
118
+ ## Installation
119
+
120
+ Add this line to your application's Gemfile:
121
+
122
+ ```ruby
123
+ gem 'captions'
124
+ ```
125
+
126
+ Or install it yourself as:
127
+
128
+ $ gem install captions
129
+
130
+
131
+ ## Development
132
+
133
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
134
+
135
+ 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).
136
+
137
+ ## Contributing
138
+
139
+ Bug reports and pull requests are welcome on GitHub at https://github.com/navinre/captions.
140
+
141
+
142
+ ## License
143
+
144
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
145
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'captions/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "amagi-captions"
8
+ spec.version = Captions::VERSION
9
+ spec.authors = ["navin"]
10
+ spec.email = ["cloudport.team@amagi.com"]
11
+
12
+ spec.summary = %q{Subtitle Editor and Converter written in Ruby}
13
+ spec.description = %q{Subtitle Editor and Converter written in Ruby. Captions can read/modify/export subtitles from one format to another }
14
+ spec.homepage = "https://github.com/amagimedia/captions"
15
+ spec.license = "MIT"
16
+
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"]
23
+
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "captions"
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 "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,179 @@
1
+ module Captions
2
+ class Base
3
+
4
+ # Adding Util methods so that it can be used by
5
+ # any subtitle parser
6
+ include Util
7
+
8
+ # Creates new instance of parser
9
+ # Usage:
10
+ # p = Captions::Base.new(nil, 25)
11
+ #
12
+ # This creates new cue-list with no file object
13
+ # associated with it. `parse` method cannot be
14
+ # called if no file path is specified.
15
+ #
16
+ # p = Captions::Base.new('file_path', 25)
17
+ #
18
+ # This parses the file specified in the file path
19
+ # `parse` method will be defined in any one of the
20
+ # classes which extends this base class. This parses
21
+ # the file in the fps specified. If a subtitle file
22
+ # has to be parsed in different fps, it can be passed
23
+ # as a parameter. FPS parameter plays an important
24
+ # role if the start-time or end-time of a subtitle is
25
+ # mentioned in frames (i.e) HH:MM:SS:FF (frames)
26
+ def initialize(file=nil, fps=25)
27
+ @cue_list = CueList.new(fps)
28
+ @file = File.new(file, 'r:bom|utf-8') if file
29
+ end
30
+
31
+ # This overrides the existing cuelist which has been
32
+ # populated with a new cuelist. This is mainly used in
33
+ # export of one format to another.
34
+ def cues=(cue_list)
35
+ @cue_list = CueList.new(@cue_list.fps, cue_list)
36
+ end
37
+
38
+ ############## BEGINNING OF OPERATIONS #################
39
+ ## Following Operations can performed on subtitles
40
+
41
+ # A subtitle is parsed with 25 fps by default. This default
42
+ # value can be changed when creating a new parser class.
43
+ # When the subtitle is being parsed, it takes the value
44
+ # mentioned when the class is created. Even after the
45
+ # subtitle is parsed, frame rate (fps) can be changed using
46
+ # this method.
47
+ # Usage:
48
+ # p = Captions::Base.new('file_path', 25)
49
+ # p.parse
50
+ # p.set_frame_rate(29.97)
51
+ #
52
+ # This method changes all the subtitle which are parsed to
53
+ # the new frame rate.
54
+ def set_frame_rate(rate)
55
+ @cue_list.frame_rate = rate
56
+ end
57
+
58
+ # This returns the subtitle which are parsed. A block
59
+ # can also be passed to filter the cues based on following
60
+ # parameters.
61
+ # Usage:
62
+ # p.cues
63
+ # p.cues { |c| c.start_time > 1000 }
64
+ # p.cues { |c| c.end_time > 1000 }
65
+ # p.cues { |c| c.duration > 1000 }
66
+ #
67
+ # Filters based on the condition and returns new set of cues
68
+ def cues(&block)
69
+ if block_given?
70
+ base = self.class.new()
71
+ base.cues = fetch_result(&block)
72
+ return base.cues
73
+ else
74
+ @cue_list
75
+ end
76
+ end
77
+
78
+ # This filters the subtitle based on the condition and returns
79
+ # a new object. A condition is mandatory to filter subtitles
80
+ # Usage:
81
+ # p.filter
82
+ # p.filter { |c| c.start_time > 1000 }
83
+ # p.filter { |c| c.end_time > 1000 }
84
+ # p.filter { |c| c.duration > 1000 }
85
+ #
86
+ # Filters and returns a Captions class rather than a cue list
87
+ def filter(&block)
88
+ if block_given?
89
+ base = self.class.new()
90
+ base.cues = fetch_result(&block)
91
+ return base
92
+ end
93
+ end
94
+
95
+ # Moves subtitles by `n` milliseconds
96
+ # Usage:
97
+ # p.move_by(1000)
98
+ # p.move_by("00:00:02.000")
99
+ # p.move_by(1000) { |c| c.start_time > 2000 }
100
+ #
101
+ # This changes start-time and end-time of subtiltes by
102
+ # the time passed.
103
+ def move_by(diff, &block)
104
+ msec = sanitize(diff, frame_rate)
105
+ fetch_result(&block).each do |cue|
106
+ cue.start_time += msec
107
+ cue.end_time += msec
108
+ end
109
+ end
110
+
111
+ # Increases duration of subtitles by `n` milliseconds
112
+ # Usage:
113
+ # p.increase_duration_by(1000)
114
+ # p.increase_duration_by("00:00:02.000")
115
+ # p.increase_duration_by(1000) { |c| c.start_time > 2000 }
116
+ #
117
+ # This increases duration of subtiltes by the time passed.
118
+ def increase_duration_by(diff, &block)
119
+ msec = sanitize(diff, frame_rate)
120
+ fetch_result(&block).each do |cue|
121
+ cue.duration += msec
122
+ end
123
+ end
124
+
125
+ ################ END OF OPERATIONS ###################
126
+
127
+ private
128
+
129
+ # This is the base method through which all subtitle
130
+ # file parsing should be done. This throws error if
131
+ # no `@file` is found. This also closes the file once
132
+ # the file has be parsed. All other logic like when the
133
+ # subtitle has to be inserted into the list, when to set
134
+ # the start and end time will be defined inside `parse`
135
+ # method.
136
+ def base_parser
137
+ raise UnknownFile, "No subtitle file specified" if @file.nil?
138
+ begin
139
+ yield
140
+ ensure
141
+ @file.close
142
+ end
143
+ return true
144
+ end
145
+
146
+ # This is the base method through which all subtitle
147
+ # exports has to be done. When a `file_path` is passed
148
+ # to this base method, it opens the file. Other logic
149
+ # about how the file should be written is defined inside
150
+ # `dump` method in one of the subclass.
151
+ def base_dump(file)
152
+ begin
153
+ File.open(file, 'w') do |file|
154
+ yield(file)
155
+ end
156
+ end
157
+ return true
158
+ end
159
+
160
+ # This returns the frame-rate which was used for parsing
161
+ # the subtitles
162
+ def frame_rate
163
+ @cue_list.fps
164
+ end
165
+
166
+ # This accepts a block and returns the result based
167
+ # on the condition passed. This acts on the cuelist
168
+ # whenever a block is passed it does select with the
169
+ # condition passed and returns the result. When no
170
+ # block is passed, it returns the entire cuelist.
171
+ def fetch_result(&block)
172
+ if block_given?
173
+ return @cue_list.select(&block)
174
+ else
175
+ return @cue_list
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,93 @@
1
+ module Captions
2
+ class Cue
3
+ include Util
4
+
5
+ # Text Properties supported
6
+ ALIGNMENT = "alignment"
7
+ COLOR = "color"
8
+ POSITION = "position"
9
+
10
+ # List of Text Properties
11
+ TEXT_PROPERTIES = [ALIGNMENT, COLOR, POSITION]
12
+
13
+ attr_accessor :number, :start_time, :end_time, :duration, :text, :properties
14
+
15
+ # Creates a new Cue class
16
+ # Each cue denotes a subtitle.
17
+ def initialize(cue_number = nil)
18
+ self.text = nil
19
+ self.start_time = nil
20
+ self.end_time = nil
21
+ self.duration = nil
22
+ self.number = cue_number
23
+ self.properties = {}
24
+ end
25
+
26
+ # Sets the time for the cue. Both start-time and
27
+ # end-time can be passed together. This just assigns
28
+ # the value passed.
29
+ def set_time(start_time, end_time, duration = nil)
30
+ self.start_time = start_time
31
+ self.end_time = end_time
32
+ self.duration = duration
33
+ end
34
+
35
+ # Getter and Setter methods for Text Properties
36
+ # These are pre-defined properties. This is just to assign
37
+ # or access the properties of that text.
38
+ TEXT_PROPERTIES.each do |setting|
39
+ define_method :"#{setting}" do
40
+ if self.properties[setting].present?
41
+ return self.properties[setting]
42
+ end
43
+ return nil
44
+ end
45
+
46
+ define_method :"#{setting}=" do |value|
47
+ self.properties[setting] = value
48
+ end
49
+ end
50
+
51
+ # Serializes the values set for the cue.
52
+ # Converts start-time, end-time and duration to milliseconds
53
+ # If duration is not found, it will be calculated based on
54
+ # start-time and end-time.
55
+ def serialize(fps)
56
+ raise InvalidSubtitle, "Subtitle should have start time" if self.start_time.nil?
57
+ raise InvalidSubtitle, "Subtitle shold have end time" if self.end_time.nil?
58
+
59
+ begin
60
+ ms_per_frame = (1000.0 / fps)
61
+ self.start_time = convert_to_msec(self.start_time, ms_per_frame)
62
+ self.end_time = convert_to_msec(self.end_time, ms_per_frame)
63
+ if duration.nil?
64
+ self.duration = self.end_time - self.start_time
65
+ else
66
+ self.duration = convert_to_msec(self.duration, ms_per_frame)
67
+ end
68
+ rescue
69
+ raise InvalidSubtitle, "Cannot calculate start-time or end-time"
70
+ end
71
+ end
72
+
73
+ # Changes start-time, end-time and duration based on new frame-rate
74
+ def change_frame_rate(old_rate, new_rate)
75
+ self.start_time = convert_frame_rate(self.start_time, old_rate, new_rate)
76
+ self.end_time = convert_frame_rate(self.end_time, old_rate, new_rate)
77
+ self.duration = convert_frame_rate(self.duration, old_rate, new_rate)
78
+ end
79
+
80
+ # Adds text. If text is already found, new-line is appended.
81
+ def add_text(text)
82
+ if self.text.nil?
83
+ self.text = text
84
+ else
85
+ self.text += "\n" + text
86
+ end
87
+ end
88
+
89
+ def <=>(other_cue)
90
+ self.start_time <=> other_cue.start_time
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,27 @@
1
+ module Captions
2
+
3
+ # Used for all execptions thrown from captions
4
+ class CaptionsError < StandardError
5
+ end
6
+
7
+ # Subtitle has to be in correct format
8
+ # when its being serialized
9
+ class InvalidSubtitle < CaptionsError
10
+ end
11
+
12
+ # Subtitle has to be correct format
13
+ # when its being parsed
14
+ class MalformedString < CaptionsError
15
+ end
16
+
17
+ # File has to be specifed for parsing
18
+ # subtitle
19
+ class UnknownFile < CaptionsError
20
+ end
21
+
22
+ # Input has to be proper for filtering
23
+ # subtitles
24
+ class InvalidInput < CaptionsError
25
+ end
26
+
27
+ end
@@ -0,0 +1,25 @@
1
+ module Captions
2
+
3
+ # This module adds support for export of subtitles
4
+ # from one format to another. Subclasses which
5
+ # extends Captions::Base needs to `dump` method to
6
+ # support export to that corresponding format
7
+ module Export
8
+ Captions.constants.each do |format|
9
+ obj = Captions.const_get(format)
10
+ next unless obj.is_a?(Class) and (obj.superclass == Captions::Base)
11
+ method_name = "export_to_" + format.to_s.downcase
12
+ define_method(method_name) do |file, cues=nil|
13
+ sub_format = obj.new()
14
+ sub_format.cues = cues || self.cues.dup
15
+ sub_format.dump(file) if sub_format.respond_to?(:dump)
16
+ end
17
+ end
18
+ end
19
+
20
+ # Including this module after all the sub-classes of
21
+ # Captions::Base has been defined.
22
+ Captions::Base.send(:include, Export)
23
+
24
+ end
25
+
@@ -0,0 +1,83 @@
1
+ module Captions
2
+ class SRT < Base
3
+
4
+ def parse
5
+ base_parser do
6
+ count = 0
7
+ state = :new_cue
8
+ cue = nil
9
+ loop do
10
+ count += 1
11
+ line = @file.gets
12
+ break if line.nil? ## End of file
13
+ line.chomp!
14
+ case state
15
+ when :new_cue
16
+ line.strip!
17
+ next if line.empty? ## just another blank line, remain in new_cue state
18
+ begin
19
+ cue = Cue.new(Integer(line))
20
+ rescue ArgumentError
21
+ raise InvalidSubtitle, "Invalid Cue Number at line #{count}"
22
+ end
23
+ state = :time
24
+ when :time
25
+ line.strip!
26
+ raise InvalidSubtitle, "Invalid Time Format at line #{count}" unless is_time?(line)
27
+ start_time, end_time = get_time(line)
28
+ cue.set_time(start_time, end_time)
29
+ state = :text
30
+ when :text
31
+ if line.empty?
32
+ ## end of previous cue
33
+ @cue_list.append(cue) if cue && cue.start_time
34
+ cue = nil
35
+ state = :new_cue
36
+ else
37
+ line.strip!
38
+ cue.add_text(line)
39
+ end
40
+ end
41
+ end
42
+ @cue_list.append(cue) if cue && cue.start_time
43
+ end
44
+ end
45
+
46
+ def dump(file)
47
+ base_dump(file) do |file|
48
+ @cue_list.each do |cue|
49
+ file.write(cue.number)
50
+ file.write("\n")
51
+ file.write(msec_to_timecode(cue.start_time).gsub!('.' , ','))
52
+ file.write(" --> ")
53
+ file.write(msec_to_timecode(cue.end_time).gsub!('.' , ','))
54
+ file.write("\n")
55
+ file.write(cue.text)
56
+ file.write("\n\n")
57
+ end
58
+ end
59
+ end
60
+
61
+ def get_time(line)
62
+ data = line.split('-->')
63
+ return format_time(data[0]), format_time(data[1])
64
+ end
65
+
66
+ def format_time(text)
67
+ text.strip.gsub(/,/,".")
68
+ end
69
+
70
+ def is_number?(text)
71
+ !!text.match(/^\d+$/)
72
+ end
73
+
74
+ def is_time?(text)
75
+ !!text.match(/^\d{2}:\d{2}:\d{2},\d{3}.*\d{2}:\d{2}:\d{2},\d{3}$/)
76
+ end
77
+
78
+ def is_text?(text)
79
+ !text.empty? and text.is_a?(String)
80
+ end
81
+
82
+ end
83
+ end