tbmx 0.1.0 → 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.
Files changed (2) hide show
  1. data/lib/tbmx.rb +253 -77
  2. metadata +2 -2
data/lib/tbmx.rb CHANGED
@@ -36,111 +36,287 @@
36
36
  # POSSIBILITY OF SUCH DAMAGE.
37
37
 
38
38
  require 'active_support/all'
39
+ require 'monkey-patch'
39
40
 
40
41
  include ERB::Util
41
42
 
42
43
  module TBMX
43
- class HTML
44
- attr_reader :text, :lines, :paragraphs
44
+ class ParseError < RuntimeError
45
+ end
46
+
47
+ class ParserNode
48
+ end
49
+
50
+ class Token < ParserNode
51
+ class << self
52
+ # The child classes should implement this method. If there is an
53
+ # immediate match, they should return a newly-created instance of
54
+ # themselves and the rest of the input as a string. If there is no match,
55
+ # they should return nil.
56
+ def matches? text
57
+ raise NotImplementedError,
58
+ "Child class #{self.class} should implement this."
59
+ end
60
+ end
61
+ end
62
+
63
+ class SingleCharacterToken < Token
64
+ def text
65
+ self.class.character_matched
66
+ end
67
+
68
+ class << self
69
+ def character_matched
70
+ self::CHARACTER_MATCHED
71
+ end
45
72
 
73
+ def matches? text
74
+ if text.first == character_matched
75
+ return [self.new, text.rest]
76
+ else
77
+ return nil
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ class StringToken < Token
84
+ attr_reader :text
46
85
  def initialize(text)
47
86
  raise ArgumentError if not text.is_a? String
87
+ raise ArgumentError if not text =~ self.class.full_match_regex
48
88
  @text = text
49
- @lines = text.split("\n").map &:strip
50
- @paragraphs = @lines.split ""
51
- end
52
-
53
- def evaluate_command(command, input)
54
- case command
55
- when "\\"
56
- "\\ #{input}"
57
- when "b"
58
- "<b>#{input}</b>"
59
- when "i"
60
- "<i>#{input}</i>"
61
- when "sub"
62
- "<sub>#{input}</sub>"
63
- when "sup"
64
- "<sup>#{input}</sup>"
65
- else
66
- "<b>[UNKNOWN COMMAND #{command}]</b>"
67
- end
68
89
  end
69
90
 
70
- def parse_command(command, rest)
71
- if rest == nil or rest == ""
72
- return [evaluate_command(command, rest),
73
- ""]
74
- elsif rest[0] =~ /\s/
75
- return [evaluate_command(command, ""),
76
- rest[1 .. -1]]
77
- elsif rest[0] == "{"
78
- if backslash = rest.index("\\")
79
- if (closing_brace = rest.index("}")) < backslash
80
- return [evaluate_command(command, rest[1 ... closing_brace]),
81
- rest[ closing_brace+1 .. -1]]
82
- elsif closing_brace
83
- interior = parse_command_body(backslash, rest)
84
- if new_closing_brace = interior.index("}")
85
- return [evaluate_command(command,
86
- interior[1 ... new_closing_brace]),
87
- interior[ new_closing_brace+1 .. -1]]
88
- else # Assume an implied closing brace.
89
- return [evaluate_command(command, interior[1 .. -1]),
90
- ""]
91
- end
91
+ def to_html
92
+ @text
93
+ end
94
+
95
+ class << self
96
+ def full_match_regex
97
+ self::FULL_MATCH_REGEX # Define this in a child class.
98
+ end
99
+
100
+ def front_match_regex
101
+ self::FRONT_MATCH_REGEX # Define this in a child class.
102
+ end
103
+
104
+ def count_regex
105
+ self::COUNT_REGEX # Define this in a child class.
106
+ end
107
+
108
+ def matches? text
109
+ if text =~ front_match_regex
110
+ count = text.index count_regex
111
+ if count.nil?
112
+ return [self.new(text), ""]
92
113
  else
93
- return evaluate_command(command, parse_command_body(backslash, rest))
114
+ return [self.new(text[0 ... count]), text[count .. -1]]
94
115
  end
95
- elsif closing_brace = rest.index("}")
96
- return [evaluate_command(command, rest[1 ... closing_brace]),
97
- rest[ closing_brace+1 .. -1]]
98
- else # Assume an implied closing brace.
99
- return [evaluate_command(command, rest[1 .. -1]),
100
- ""]
116
+ else
117
+ return nil
101
118
  end
102
- else
103
- raise RuntimeError, "Unreachable: probably a bug in parse_command_name."
104
119
  end
105
120
  end
121
+ end
106
122
 
107
- def parse_command_name(input)
108
- if end_command = input.index(/[\{\s]/)
109
- return [input[0 ... end_command],
110
- input[ end_command .. -1]]
111
- else
112
- return [input, ""]
123
+ class BackslashToken < SingleCharacterToken
124
+ CHARACTER_MATCHED = "\\"
125
+ end
126
+
127
+ class LeftBraceToken < SingleCharacterToken
128
+ CHARACTER_MATCHED = "{"
129
+ end
130
+
131
+ class RightBraceToken < SingleCharacterToken
132
+ CHARACTER_MATCHED = "}"
133
+ end
134
+
135
+
136
+ class EmptyNewlinesToken < StringToken
137
+ FULL_MATCH_REGEX = /\A\n\n+\z/
138
+ FRONT_MATCH_REGEX = /\A\n\n+/
139
+ COUNT_REGEX = /[^\n]/
140
+
141
+ def newlines
142
+ text
143
+ end
144
+ end
145
+
146
+ class WhitespaceToken < StringToken
147
+ FULL_MATCH_REGEX = /\A\s+\z/
148
+ FRONT_MATCH_REGEX = /\A\s+/
149
+ COUNT_REGEX = /\S/
150
+
151
+ def whitespace
152
+ text
153
+ end
154
+
155
+ def to_html
156
+ " " # Replace all whitespace tokens with a single space.
157
+ end
158
+ end
159
+
160
+ class WordToken < StringToken
161
+ FULL_MATCH_REGEX = /\A[^\s{}\\]+\z/
162
+ FRONT_MATCH_REGEX = /[^\s{}\\]+/
163
+ COUNT_REGEX = /[\s{}\\]/
164
+
165
+ def word
166
+ text
167
+ end
168
+ end
169
+
170
+ class Tokenizer
171
+ attr_reader :text, :tokens
172
+ def initialize(text)
173
+ @text = text
174
+ tokenize
175
+ end
176
+
177
+ def tokenize
178
+ @tokens = []
179
+ rest = text
180
+ while rest.length > 0
181
+ if result = BackslashToken.matches?(rest) or # Single Character Tokens
182
+ result = LeftBraceToken.matches?(rest) or
183
+ result = RightBraceToken.matches?(rest) or
184
+ result = EmptyNewlinesToken.matches?(rest) or # String Tokens
185
+ result = WhitespaceToken.matches?(rest) or
186
+ result = WordToken.matches?(rest)
187
+ then
188
+ @tokens << result[0]
189
+ rest = result[1]
190
+ else
191
+ raise RuntimeError, "Couldn't tokenize the remaining text."
192
+ end
113
193
  end
194
+ return @tokens
114
195
  end
196
+ end
115
197
 
116
- def parse_command_body(backslash, input)
117
- before = input[0 ... backslash]
118
- command, rest = parse_command_name input[backslash+1 .. -1]
119
- middle, back = parse_command(command, rest)
120
- return before + middle + parse_paragraph_body(back)
198
+ class CommandParser < ParserNode
199
+ attr_reader :command, :expressions
200
+
201
+ def initialize(command, expressions)
202
+ raise ArgumentError if not command.is_a? WordToken
203
+ @command = command
204
+ raise ArgumentError if not expressions.is_a? Array
205
+ expressions.each {|expression| raise ArgumentError if not expression.kind_of? ParserNode}
206
+ @expressions = expressions
121
207
  end
122
208
 
123
- def parse_paragraph_body(body)
124
- return "" if body.nil? or body == ""
125
- raise ArgumentError, "Body is #{body.class}" if not body.is_a? String
126
- if backslash = body.index("\\")
127
- return parse_command_body backslash, body
209
+ def to_html
210
+ case command.word
211
+ when "backslash", "bslash"
212
+ "\\"
213
+ when "left-brace", "left_brace", "leftbrace", "lbrace"
214
+ "{"
215
+ when "right-brace", "right_brace", "rightbrace", "rbrace"
216
+ "}"
217
+ when "br", "newline"
218
+ "\n</br>\n"
219
+ when "bold", "b"
220
+ "<b>" + expressions.map(&:to_html).join + "</b>"
221
+ when "italic", "i"
222
+ "<i>" + expressions.map(&:to_html).join + "</i>"
223
+ when "underline", "u"
224
+ "<u>" + expressions.map(&:to_html).join + "</u>"
225
+ when "subscript", "sub"
226
+ "<sub>" + expressions.map(&:to_html).join + "</sub>"
227
+ when "superscript", "sup"
228
+ "<sup>" + expressions.map(&:to_html).join + "</sup>"
128
229
  else
129
- return body
230
+ %{<span style="color: red">[UNKNOWN COMMAND #{command.to_html}]}
231
+ end
232
+ end
233
+
234
+ class << self
235
+ def parse(tokens)
236
+ expressions = []
237
+ rest = tokens
238
+ backslash, command, left_brace = rest.shift(3)
239
+ right_brace = nil
240
+ raise ParseError if not backslash.is_a? BackslashToken
241
+ raise ParseError if not command.is_a? WordToken
242
+ if not left_brace.is_a? LeftBraceToken # A command with no interior.
243
+ rest.unshift left_brace if not left_brace.is_a? WhitespaceToken
244
+ return [CommandParser.new(command, []), rest]
245
+ end
246
+ while rest.length > 0
247
+ if rest.first.is_a? WordToken
248
+ expressions << rest.shift
249
+ elsif rest.first.is_a? WhitespaceToken
250
+ expressions << rest.shift
251
+ elsif rest.first.is_a? BackslashToken
252
+ result, rest = CommandParser.parse(rest)
253
+ expressions << result
254
+ elsif rest.first.is_a? RightBraceToken
255
+ right_brace = rest.shift
256
+ return [CommandParser.new(command, expressions), rest]
257
+ else
258
+ raise ParseError
259
+ end
260
+ end
261
+ if right_brace.nil? # Allow a forgotten final right brace.
262
+ return [CommandParser.new(command, expressions), rest]
263
+ end
130
264
  end
131
265
  end
266
+ end
132
267
 
133
- def parse_paragraph(paragraph)
134
- raise ArgumentError if not paragraph.is_a? Array
135
- paragraph.each {|line| raise ArgumentError if not line.is_a? String}
136
- body = paragraph.map do |line|
137
- html_escape line
138
- end.join "\n"
139
- "<p>#{parse_paragraph_body body}</p>"
268
+ class ParagraphParser < ParserNode
269
+ attr_reader :expressions, :tokens
270
+
271
+ def initialize(tokens)
272
+ raise ArgumentError if not tokens.is_a? Array
273
+ tokens.each {|token| raise ArgumentError if not token.kind_of? ParserNode}
274
+ @tokens = tokens
275
+ parse
140
276
  end
141
277
 
142
278
  def parse
143
- paragraphs.map {|paragraph| parse_paragraph paragraph}.join "\n\n"
279
+ @expressions = []
280
+ rest = tokens
281
+ while rest.length > 0
282
+ if rest.first.is_a? WordToken
283
+ @expressions << rest.shift
284
+ elsif rest.first.is_a? WhitespaceToken
285
+ @expressions << rest.shift
286
+ elsif rest.first.is_a? BackslashToken
287
+ command, rest = CommandParser.parse(rest)
288
+ @expressions << command
289
+ else
290
+ return self
291
+ end
292
+ end
293
+ end
294
+
295
+ def to_html
296
+ "<p>\n" + expressions.map(&:to_html).join + "\n</p>\n"
297
+ end
298
+ end
299
+
300
+ class Parser < ParserNode
301
+ attr_reader :paragraphs, :split_tokens, :text, :tokenizer
302
+
303
+ def tokens
304
+ tokenizer.tokens
305
+ end
306
+
307
+ def initialize(text)
308
+ @text = text
309
+ @tokenizer = Tokenizer.new text
310
+ parse
311
+ end
312
+
313
+ def parse
314
+ @split_tokens = tokens.split {|token| token.class == EmptyNewlinesToken}
315
+ @paragraphs = @split_tokens.map {|split_tokens| ParagraphParser.new split_tokens}
316
+ end
317
+
318
+ def to_html
319
+ paragraphs.map(&:to_html).join "\n"
144
320
  end
145
321
  end
146
322
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tbmx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-12-04 00:00:00.000000000 Z
12
+ date: 2013-12-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport