tbmx 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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