teepee 0.5.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/teepee.rb +568 -0
  3. metadata +71 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a73a0f9bde7b2730bce0660061804f994f2950c3
4
+ data.tar.gz: c0ee7a79b3cb0a133f23f019d1abfa9d050843ba
5
+ SHA512:
6
+ metadata.gz: 44af78ff299e1b02e24c15fe5aaed077b551e4d7aa0f131e1c8667cbd7da5240c961c3860713dbb2b0eca4e9d2023f86d21cb20a0b5bb284411a47e55bd7db51
7
+ data.tar.gz: 9d5d69e4462020eb75bf3807d77f65bbdb9d7db2f71597743a6f8c268ac29a54a1116b6547a79876b8ba4162ee6f6d178eb7043dbdccb85ac50638d325a42438
data/lib/teepee.rb ADDED
@@ -0,0 +1,568 @@
1
+ # -*- coding: utf-8 -*-
2
+ # -*- mode: Ruby -*-
3
+
4
+ # Copyright © 2013-2015, Christopher Mark Gore,
5
+ # Soli Deo Gloria,
6
+ # All rights reserved.
7
+ #
8
+ # 2317 South River Road, Saint Charles, Missouri 63303 USA.
9
+ # Web: http://cgore.com
10
+ # Email: cgore@cgore.com
11
+ #
12
+ # Redistribution and use in source and binary forms, with or without
13
+ # modification, are permitted provided that the following conditions are met:
14
+ #
15
+ # * Redistributions of source code must retain the above copyright
16
+ # notice, this list of conditions and the following disclaimer.
17
+ #
18
+ # * Redistributions in binary form must reproduce the above copyright
19
+ # notice, this list of conditions and the following disclaimer in the
20
+ # documentation and/or other materials provided with the distribution.
21
+ #
22
+ # * Neither the name of Christopher Mark Gore nor the names of other
23
+ # contributors may be used to endorse or promote products derived from
24
+ # this software without specific prior written permission.
25
+ #
26
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
27
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
30
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
31
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
32
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
33
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
35
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
36
+ # POSSIBILITY OF SUCH DAMAGE.
37
+
38
+ require 'active_support/all'
39
+ require 'monkey-patch'
40
+
41
+ include ERB::Util
42
+
43
+ module Teepee
44
+ TB_COM = "http://thinkingbicycle.com"
45
+
46
+ module MathFunctions
47
+ class << self
48
+ def degrees2radians degrees
49
+ degrees * Math::PI / 180.0
50
+ end
51
+
52
+ def lgamma n
53
+ Math::lgamma(n).first
54
+ end
55
+
56
+ def radians2degrees radians
57
+ radians * 180.0 / Math::PI
58
+ end
59
+ end
60
+ end
61
+
62
+ class ParseError < RuntimeError
63
+ end
64
+
65
+ class ParserNode
66
+ end
67
+
68
+ class Token < ParserNode
69
+ class << self
70
+ # The child classes should implement this method. If there is an
71
+ # immediate match, they should return a newly-created instance of
72
+ # themselves and the rest of the input as a string. If there is no match,
73
+ # they should return nil.
74
+ def matches? text
75
+ raise NotImplementedError,
76
+ "Child class #{self.class} should implement this."
77
+ end
78
+ end
79
+ end
80
+
81
+ class SingleCharacterToken < Token
82
+ def text
83
+ self.class.character_matched
84
+ end
85
+
86
+ class << self
87
+ def character_matched
88
+ self::CHARACTER_MATCHED
89
+ end
90
+
91
+ def matches? text
92
+ if text.first == character_matched
93
+ return [self.new, text.rest]
94
+ else
95
+ return nil
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ class StringToken < Token
102
+ attr_reader :text
103
+
104
+ def initialize(text)
105
+ raise ArgumentError if not text.is_a? String
106
+ raise ArgumentError if not text =~ self.class.full_match_regex
107
+ @text = text
108
+ end
109
+
110
+ def to_s
111
+ @text
112
+ end
113
+
114
+ def to_html
115
+ @text
116
+ end
117
+
118
+ class << self
119
+ def full_match_regex
120
+ self::FULL_MATCH_REGEX # Define this in a child class.
121
+ end
122
+
123
+ def front_match_regex
124
+ self::FRONT_MATCH_REGEX # Define this in a child class.
125
+ end
126
+
127
+ def count_regex
128
+ self::COUNT_REGEX # Define this in a child class.
129
+ end
130
+
131
+ def matches? text
132
+ if text =~ front_match_regex
133
+ count = text.index count_regex
134
+ if count.nil?
135
+ return [self.new(text), ""]
136
+ else
137
+ return [self.new(text[0 ... count]), text[count .. -1]]
138
+ end
139
+ else
140
+ return nil
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ class BackslashToken < SingleCharacterToken
147
+ CHARACTER_MATCHED = "\\"
148
+ end
149
+
150
+ class LeftBraceToken < SingleCharacterToken
151
+ CHARACTER_MATCHED = "{"
152
+ end
153
+
154
+ class RightBraceToken < SingleCharacterToken
155
+ CHARACTER_MATCHED = "}"
156
+ end
157
+
158
+
159
+ class EmptyNewlinesToken < StringToken
160
+ FULL_MATCH_REGEX = /\A\n\n+\z/
161
+ FRONT_MATCH_REGEX = /\A\n\n+/
162
+ COUNT_REGEX = /[^\n]/
163
+
164
+ def newlines
165
+ text
166
+ end
167
+ end
168
+
169
+ class WhitespaceToken < StringToken
170
+ FULL_MATCH_REGEX = /\A\s+\z/
171
+ FRONT_MATCH_REGEX = /\A\s+/
172
+ COUNT_REGEX = /\S/
173
+
174
+ def whitespace
175
+ text
176
+ end
177
+
178
+ def to_html
179
+ " " # Replace all whitespace tokens with a single space.
180
+ end
181
+ end
182
+
183
+ class WordToken < StringToken
184
+ FULL_MATCH_REGEX = /\A[^\s{}\\]+\z/
185
+ FRONT_MATCH_REGEX = /[^\s{}\\]+/
186
+ COUNT_REGEX = /[\s{}\\]/
187
+
188
+ def to_html
189
+ html_escape text
190
+ end
191
+
192
+ def word
193
+ text
194
+ end
195
+ end
196
+
197
+ class NumberToken < Token
198
+ attr_reader :number, :text
199
+
200
+ def initialize(text)
201
+ raise ArgumentError if not text.is_a? String
202
+ @text = text
203
+ end
204
+
205
+ def parse
206
+ end
207
+
208
+ def to_s
209
+ number.to_s
210
+ end
211
+
212
+ def to_html
213
+ to_s
214
+ end
215
+
216
+ class << self
217
+ def matches? text
218
+ end
219
+ end
220
+ end
221
+
222
+ class Tokenizer
223
+ attr_reader :text, :tokens
224
+ def initialize(text)
225
+ @text = text
226
+ tokenize
227
+ end
228
+
229
+ def tokenize
230
+ @tokens = []
231
+ rest = text.gsub("\r", "")
232
+ while rest.length > 0
233
+ if result = BackslashToken.matches?(rest) or # Single Character Tokens
234
+ result = LeftBraceToken.matches?(rest) or
235
+ result = RightBraceToken.matches?(rest) or
236
+ result = EmptyNewlinesToken.matches?(rest) or # String Tokens
237
+ result = WhitespaceToken.matches?(rest) or
238
+ result = NumberToken.matches?(rest) or
239
+ result = WordToken.matches?(rest)
240
+ then
241
+ @tokens << result[0]
242
+ rest = result[1]
243
+ else
244
+ raise RuntimeError, "Couldn't tokenize the remaining text."
245
+ end
246
+ end
247
+ return @tokens
248
+ end
249
+ end
250
+
251
+ class CommandParser < ParserNode
252
+ attr_reader :command, :expressions
253
+
254
+ def initialize(command, expressions)
255
+ raise ArgumentError if not command.is_a? WordToken
256
+ @command = command
257
+ raise ArgumentError if not expressions.is_a? Array
258
+ expressions.each {|expression| raise ArgumentError if not expression.kind_of? ParserNode}
259
+ @expressions = expressions
260
+ end
261
+
262
+ def command_error(message)
263
+ %{<span style="color: red">[#{message}]</span>}
264
+ end
265
+
266
+ def to_html
267
+ case command.word
268
+ when "backslash", "bslash"
269
+ "\\"
270
+ when "left-brace", "left_brace", "leftbrace", "lbrace", "opening-brace", "opening_brace",
271
+ "openingbrace", "obrace"
272
+ "{"
273
+ when "right-brace", "right_brace", "rightbrace", "rbrace", "closing-brace", "closing_brace",
274
+ "closingbrace", "cbrace"
275
+ "}"
276
+ when "br", "newline"
277
+ "\n</br>\n"
278
+ when "bold", "b", "textbf"
279
+ html_tag :b
280
+ when "del", "s", "strike", "strikethrough", "strikeout"
281
+ html_tag :del
282
+ when "italic", "i", "textit", "it"
283
+ html_tag :i
284
+ when "underline", "u"
285
+ html_tag :u
286
+ when "tt", "texttt", "teletype", "typewriter"
287
+ html_tag :tt
288
+ when "small"
289
+ html_tag :small
290
+ when "big"
291
+ html_tag :big
292
+ when "subscript", "sub"
293
+ html_tag :sub
294
+ when "superscript", "sup"
295
+ html_tag :sup
296
+ when "user", "user-id", "user_id"
297
+ user_command_handler
298
+ when "link-id", "link_id"
299
+ link_id_command_handler
300
+ when "keyword-id", "keyword_id"
301
+ keyword_id_command_handler
302
+ when "tag-id", "tag_id"
303
+ tag_id_command_handler
304
+ when "forum-id", "forum_id"
305
+ forum_id_command_handler
306
+ when "folder-id", "folder_id"
307
+ folder_id_command_handler
308
+ when "bookmarks-folder-id", "bookmarks_folder_id", "bookmarks_folder-id", "bookmarks-folder_id",
309
+ "bookmark-folder-id", "bookmark_folder_id", "bookmark_folder-id", "bookmark-folder_id"
310
+ bookmarks_folder_id_command_handler
311
+ when "pi"
312
+ "#{Math::PI}"
313
+ when "e"
314
+ "#{Math::E}"
315
+ when "+"
316
+ injectable_math_function_handler 0, :+
317
+ when "-"
318
+ if (numbers = numbers_from_expressions).length == 1
319
+ injectable_math_function_handler 0, :-
320
+ else
321
+ reducable_math_function_handler :-
322
+ end
323
+ when "*"
324
+ injectable_math_function_handler 1, :*
325
+ when "/"
326
+ if (numbers = numbers_from_expressions).length == 1
327
+ 1 / numbers.first
328
+ else
329
+ reducable_math_function_handler :/
330
+ end
331
+ when "%"
332
+ injectable_math_function_handler numbers_from_expressions.first, :%
333
+ when "^", "**"
334
+ number, exponent = numbers_from_expressions
335
+ number.send :**, exponent
336
+ when "sin", "cos", "tan",
337
+ "asin", "acos", "atan",
338
+ "sinh", "cosh", "tanh",
339
+ "asinh", "acosh", "atanh",
340
+ "erf", "erfc",
341
+ "gamma", "log10", "sqrt"
342
+ math_function_handler command.word.to_sym
343
+ when "d2r", "deg->rad", "degrees->radians"
344
+ MathFunctions::degrees2radians number_from_expression
345
+ when "r2d", "rad->deg", "radians->degrees"
346
+ MathFunctions::radians2degrees number_from_expression
347
+ when "lgamma"
348
+ MathFunctions::lgamma number_from_expression
349
+ when "ld", "log2"
350
+ Math.log2 number_from_expression
351
+ when "ln"
352
+ Math.log number_from_expression
353
+ when "log"
354
+ base, number = numbers_from_expressions
355
+ if number.nil?
356
+ Math.log base
357
+ else
358
+ Math.log number, base
359
+ end
360
+ when "ldexp"
361
+ fraction, exponent = numbers_from_expressions
362
+ Math.ldexp fraction, exponent
363
+ when "hypot"
364
+ Math.sqrt numbers_from_expressions.map {|n| n**2}
365
+ else
366
+ command_error "unknown command #{command.to_html}"
367
+ end
368
+ end
369
+
370
+ def html_tag(tag)
371
+ "<#{tag}>" + expressions.map(&:to_html).join + "</#{tag}>"
372
+ end
373
+
374
+ def tb_href(target, string)
375
+ %{<a href="#{TB_COM}/#{target}">#{string}</a>}
376
+ end
377
+
378
+ def numbers_from_expressions
379
+ expressions
380
+ .map do |number|
381
+ begin
382
+ Float(number.to_html)
383
+ rescue ArgumentError
384
+ nil
385
+ end
386
+ end.reject &:nil?
387
+ end
388
+
389
+ def number_from_expression
390
+ numbers_from_expressions.first
391
+ end
392
+
393
+ def injectable_math_function_handler(initial, function)
394
+ numbers_from_expressions.inject initial, function
395
+ end
396
+
397
+ def reducable_math_function_handler(function)
398
+ numbers_from_expressions.reduce function
399
+ end
400
+
401
+ def math_function_handler(function)
402
+ Math.send function, numbers_from_expressions.first
403
+ end
404
+
405
+ def user_command_handler
406
+ user = expressions.select {|expr| expr.is_a? WordToken}.first
407
+ if not user
408
+ command_error "user: error: no user specified"
409
+ else
410
+ if @@action_view.kind_of? ActionView::Base
411
+ the_user = User.smart_find user.to_s
412
+ if the_user
413
+ @@action_view.render partial: 'users/name_link',
414
+ locals: {the_user: the_user}
415
+ else
416
+ command_error "unknown user #{user.to_s}"
417
+ end
418
+ else
419
+ tb_href "users/#{user}", user.to_s
420
+ end
421
+ end
422
+ end
423
+
424
+ def id_command_handler(klass,
425
+ singular = klass.to_s.camelcase_to_snakecase,
426
+ plural = singular.pluralize,
427
+ partial = "#{plural}/inline",
428
+ view="")
429
+ id = expressions.select {|expr| expr.is_a? WordToken}.first
430
+ if not id
431
+ command_error "#{singular}_id: error: no #{singular} ID specified"
432
+ elsif not id.to_s =~ /\A[0-9]+\z/
433
+ command_error "#{singular}_id: error: invalid #{singular} ID specified"
434
+ else
435
+ if @@action_view.kind_of? ActionView::Base
436
+ thing = klass.find Integer(id.to_s)
437
+ if thing
438
+ @@action_view.render partial: partial,
439
+ locals: {singular.to_sym => thing}
440
+ else
441
+ command_error "unknown #{singular} ID #{id.to_s}"
442
+ end
443
+ else
444
+ tb_href "/#{plural}/#{id.to_s}/#{view}", "#{klass} ##{id.to_s}"
445
+ end
446
+ end
447
+ end
448
+
449
+ def link_id_command_handler
450
+ id_command_handler Link
451
+ end
452
+
453
+ def tag_id_command_handler
454
+ id_command_handler Tag
455
+ end
456
+
457
+ def folder_id_command_handler
458
+ id_command_handler Folder
459
+ end
460
+
461
+ def forum_id_command_handler
462
+ id_command_handler Forum
463
+ end
464
+
465
+ def bookmarks_folder_id_command_handler
466
+ id_command_handler Folder, "folder", "folders", "folders/bookmarks_inline", "bookmarks"
467
+ end
468
+
469
+ class << self
470
+ @@action_view = nil
471
+ @@controller = nil
472
+
473
+ def parse(tokens)
474
+ expressions = []
475
+ rest = tokens
476
+ backslash, command, left_brace = rest.shift(3)
477
+ right_brace = nil
478
+ raise ParseError if not backslash.is_a? BackslashToken
479
+ raise ParseError if not command.is_a? WordToken
480
+ if not left_brace.is_a? LeftBraceToken # A command with no interior.
481
+ rest.unshift left_brace if not left_brace.is_a? WhitespaceToken
482
+ return [CommandParser.new(command, []), rest]
483
+ end
484
+ while rest.length > 0
485
+ if rest.first.is_a? WordToken
486
+ expressions << rest.shift
487
+ elsif rest.first.is_a? WhitespaceToken
488
+ expressions << rest.shift
489
+ elsif rest.first.is_a? BackslashToken
490
+ result, rest = CommandParser.parse(rest)
491
+ expressions << result
492
+ elsif rest.first.is_a? RightBraceToken
493
+ right_brace = rest.shift
494
+ return [CommandParser.new(command, expressions), rest]
495
+ else
496
+ raise ParseError
497
+ end
498
+ end
499
+ if right_brace.nil? # Allow a forgotten final right brace.
500
+ return [CommandParser.new(command, expressions), rest]
501
+ end
502
+ end
503
+
504
+ def action_view=(new)
505
+ @@action_view = new
506
+ end
507
+
508
+ def controller=(new)
509
+ @@controller = new
510
+ end
511
+ end
512
+ end
513
+
514
+ class ParagraphParser < ParserNode
515
+ attr_reader :expressions, :tokens
516
+
517
+ def initialize(tokens)
518
+ raise ArgumentError if not tokens.is_a? Array
519
+ tokens.each {|token| raise ArgumentError if not token.kind_of? ParserNode}
520
+ @tokens = tokens
521
+ parse
522
+ end
523
+
524
+ def parse
525
+ @expressions = []
526
+ rest = tokens
527
+ while rest.length > 0
528
+ if rest.first.is_a? WordToken
529
+ @expressions << rest.shift
530
+ elsif rest.first.is_a? WhitespaceToken
531
+ @expressions << rest.shift
532
+ elsif rest.first.is_a? BackslashToken
533
+ command, rest = CommandParser.parse(rest)
534
+ @expressions << command
535
+ else
536
+ return self
537
+ end
538
+ end
539
+ end
540
+
541
+ def to_html
542
+ "<p>\n" + expressions.map(&:to_html).join + "\n</p>\n"
543
+ end
544
+ end
545
+
546
+ class Parser < ParserNode
547
+ attr_reader :paragraphs, :split_tokens, :text, :tokenizer
548
+
549
+ def tokens
550
+ tokenizer.tokens
551
+ end
552
+
553
+ def initialize(text)
554
+ @text = text
555
+ @tokenizer = Tokenizer.new text
556
+ parse
557
+ end
558
+
559
+ def parse
560
+ @split_tokens = tokens.split {|token| token.class == EmptyNewlinesToken}
561
+ @paragraphs = @split_tokens.map {|split_tokens| ParagraphParser.new split_tokens}
562
+ end
563
+
564
+ def to_html
565
+ paragraphs.map(&:to_html).join "\n"
566
+ end
567
+ end
568
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: teepee
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.9
5
+ platform: ruby
6
+ authors:
7
+ - Christopher Mark Gore
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-11-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: monkey-patch
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Teepee is a markup language, loosely based on Lisp and TeX, for ThinkingBicycle.com.
42
+ email: cgore@cgore.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/teepee.rb
48
+ homepage: https://github.com/cgore/ruby-teepee
49
+ licenses: []
50
+ metadata: {}
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubyforge_project:
67
+ rubygems_version: 2.4.5.1
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Teepee is a markup language for ThinkingBicycle.com.
71
+ test_files: []