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.
- data/lib/tbmx.rb +253 -77
- 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
|
44
|
-
|
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
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
114
|
+
return [self.new(text[0 ... count]), text[count .. -1]]
|
94
115
|
end
|
95
|
-
|
96
|
-
return
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
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.
|
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-
|
12
|
+
date: 2013-12-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|