teepee 0.5.9

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 (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: []