pipin 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -10,18 +10,33 @@
10
10
  == build
11
11
 
12
12
  cd myblog
13
- #rake
14
- ruby -r pipin -e 'Pipin.command :build'
13
+ rake
15
14
 
16
15
  == todo
17
16
 
18
17
  * 原稿の入力
19
- * html, txt(hd)
20
- * to_html
18
+ * html, txt(hd)
19
+ * to_html
21
20
 
22
21
  * 原稿の検索
23
- * カテゴリ
22
+ * カテゴリ
24
23
 
25
24
  * html出力
26
- * ページ一覧
27
- * 月別ページ
25
+ * ページ一覧
26
+ * 月別ページ
27
+
28
+ == trap
29
+
30
+ * 原稿ファイルを削除した場合、削除したファイルはrakeビルド時の更新条件から除外されます
31
+ ** htmlが再出力されない場合は、そのhtmlファイルを手動で削除してからrakeを実行してください
32
+
33
+ == from tDiary
34
+
35
+ * 静的なhtmlとして保存できる
36
+ ** postのhtmlを生成するプラグインや
37
+ ** 写真とかもそのままいける
38
+ * urlの互換
39
+ * exportはレンダリングされたhtmlから
40
+ * todo: コメント...
41
+ * todo: カテゴリなどの付属情報はxml的に...
42
+
@@ -0,0 +1,906 @@
1
+ # Copyright (c) 2005, Kazuhiko <kazuhiko@fdiary.net>
2
+ # Copyright (c) 2007 Minero Aoki
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # * Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ # * Redistributions in binary form must reproduce the above
12
+ # copyright notice, this list of conditions and the following
13
+ # disclaimer in the documentation and/or other materials provided
14
+ # with the distribution.
15
+ # * Neither the name of the HikiDoc nor the names of its
16
+ # contributors may be used to endorse or promote products derived
17
+ # from this software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require "stringio"
32
+ require "strscan"
33
+ require "uri"
34
+ begin
35
+ require "syntax/convertors/html"
36
+ rescue LoadError
37
+ end
38
+
39
+ class HikiDoc
40
+ VERSION = "0.0.2" # FIXME
41
+
42
+ class Error < StandardError
43
+ end
44
+
45
+ class UnexpectedError < Error
46
+ end
47
+
48
+ def HikiDoc.to_html(src, options = {})
49
+ new(HTMLOutput.new(">"), options).compile(src)
50
+ end
51
+
52
+ def HikiDoc.to_xhtml(src, options = {})
53
+ new(HTMLOutput.new(" />"), options).compile(src)
54
+ end
55
+
56
+ def initialize(output, options = {})
57
+ @output = output
58
+ @options = default_options.merge(options)
59
+ @header_re = nil
60
+ @level = options[:level] || 1
61
+ @plugin_syntax = options[:plugin_syntax] || method(:valid_plugin_syntax?)
62
+ end
63
+
64
+ def compile(src)
65
+ @output.reset
66
+ escape_plugin_blocks(src) {|escaped|
67
+ compile_blocks escaped
68
+ @output.finish
69
+ }
70
+ end
71
+
72
+ # for backward compatibility
73
+ def to_html
74
+ $stderr.puts("warning: HikiDoc#to_html is deprecated. Please use HikiDoc.to_html or HikiDoc.to_xhtml instead.")
75
+ self.class.to_html(@output, @options)
76
+ end
77
+
78
+ private
79
+
80
+ def default_options
81
+ {
82
+ :allow_bracket_inline_image => true,
83
+ :use_wiki_name => true,
84
+ :use_not_wiki_name => true,
85
+ }
86
+ end
87
+
88
+ #
89
+ # Plugin
90
+ #
91
+
92
+ def valid_plugin_syntax?(code)
93
+ /['"]/ !~ code.gsub(/'(?:[^\\']+|\\.)*'|"(?:[^\\"]+|\\.)*"/m, "")
94
+ end
95
+
96
+ def escape_plugin_blocks(text)
97
+ s = StringScanner.new(text)
98
+ buf = ""
99
+ @plugin_blocks = []
100
+ while chunk = s.scan_until(/\{\{/)
101
+ tail = chunk[-2, 2]
102
+ chunk[-2, 2] = ""
103
+ buf << chunk
104
+ # plugin
105
+ if block = extract_plugin_block(s)
106
+ @plugin_blocks.push block
107
+ buf << "\0#{@plugin_blocks.size - 1}\0"
108
+ else
109
+ buf << "{{"
110
+ end
111
+ end
112
+ buf << s.rest
113
+ yield(buf)
114
+ end
115
+
116
+ def restore_plugin_block(str)
117
+ str.gsub(/\0(\d+)\0/) {
118
+ "{{" + plugin_block($1.to_i) + "}}"
119
+ }
120
+ end
121
+
122
+ def evaluate_plugin_block(str, buf = nil)
123
+ buf ||= @output.container
124
+ str.split(/(\0\d+\0)/).each do |s|
125
+ if s[0, 1] == "\0" and s[-1, 1] == "\0"
126
+ buf << @output.inline_plugin(plugin_block(s[1..-2].to_i))
127
+ else
128
+ buf << @output.text(s)
129
+ end
130
+ end
131
+ buf
132
+ end
133
+
134
+ def plugin_block(id)
135
+ @plugin_blocks[id] or raise UnexpectedError, "must not happen: #{id.inspect}"
136
+ end
137
+
138
+ def extract_plugin_block(s)
139
+ pos = s.pos
140
+ buf = ""
141
+ while chunk = s.scan_until(/\}\}/)
142
+ buf << chunk
143
+ buf.chomp!("}}")
144
+ if @plugin_syntax.call(buf)
145
+ return buf
146
+ end
147
+ buf << "}}"
148
+ end
149
+ s.pos = pos
150
+ nil
151
+ end
152
+
153
+ #
154
+ # Block Level
155
+ #
156
+
157
+ def compile_blocks(src)
158
+ f = LineInput.new(StringIO.new(src))
159
+ while line = f.peek
160
+ case line
161
+ when COMMENT_RE
162
+ f.gets
163
+ when HEADER_RE
164
+ compile_header f.gets
165
+ when HRULE_RE
166
+ f.gets
167
+ compile_hrule
168
+ when LIST_RE
169
+ compile_list f
170
+ when DLIST_RE
171
+ compile_dlist f
172
+ when TABLE_RE
173
+ compile_table f
174
+ when BLOCKQUOTE_RE
175
+ compile_blockquote f
176
+ when INDENTED_PRE_RE
177
+ compile_indented_pre f
178
+ when BLOCK_PRE_OPEN_RE
179
+ compile_block_pre f
180
+ else
181
+ if /^$/ =~ line
182
+ f.gets
183
+ next
184
+ end
185
+ compile_paragraph f
186
+ end
187
+ end
188
+ end
189
+
190
+ COMMENT_RE = %r<\A//>
191
+
192
+ def skip_comments(f)
193
+ f.while_match(COMMENT_RE) do |line|
194
+ end
195
+ end
196
+
197
+ HEADER_RE = /\A!+/
198
+
199
+ def compile_header(line)
200
+ @header_re ||= /\A!{1,#{7 - @level}}/
201
+ level = @level + (line.slice!(@header_re).size - 1)
202
+ title = strip(line)
203
+ @output.headline level, compile_inline(title)
204
+ end
205
+
206
+ HRULE_RE = /\A----$/
207
+
208
+ def compile_hrule
209
+ @output.hrule
210
+ end
211
+
212
+ ULIST = "*"
213
+ OLIST = "#"
214
+ LIST_RE = /\A#{Regexp.union(ULIST, OLIST)}+/
215
+
216
+ def compile_list(f)
217
+ typestack = []
218
+ level = 0
219
+ @output.list_begin
220
+ f.while_match(LIST_RE) do |line|
221
+ list_type = (line[0,1] == ULIST ? "ul" : "ol")
222
+ new_level = line.slice(LIST_RE).size
223
+ item = strip(line.sub(LIST_RE, ""))
224
+ if new_level > level
225
+ (new_level - level).times do
226
+ typestack.push list_type
227
+ @output.list_open list_type
228
+ @output.listitem_open
229
+ end
230
+ @output.listitem compile_inline(item)
231
+ elsif new_level < level
232
+ (level - new_level).times do
233
+ @output.listitem_close
234
+ @output.list_close typestack.pop
235
+ end
236
+ @output.listitem_close
237
+ @output.listitem_open
238
+ @output.listitem compile_inline(item)
239
+ elsif list_type == typestack.last
240
+ @output.listitem_close
241
+ @output.listitem_open
242
+ @output.listitem compile_inline(item)
243
+ else
244
+ @output.listitem_close
245
+ @output.list_close typestack.pop
246
+ @output.list_open list_type
247
+ @output.listitem_open
248
+ @output.listitem compile_inline(item)
249
+ typestack.push list_type
250
+ end
251
+ level = new_level
252
+ skip_comments f
253
+ end
254
+ level.times do
255
+ @output.listitem_close
256
+ @output.list_close typestack.pop
257
+ end
258
+ @output.list_end
259
+ end
260
+
261
+ DLIST_RE = /\A:/
262
+
263
+ def compile_dlist(f)
264
+ @output.dlist_open
265
+ f.while_match(DLIST_RE) do |line|
266
+ dt, dd = split_dlitem(line.sub(DLIST_RE, ""))
267
+ @output.dlist_item compile_inline(dt), compile_inline(dd)
268
+ skip_comments f
269
+ end
270
+ @output.dlist_close
271
+ end
272
+
273
+ def split_dlitem(line)
274
+ re = /\A((?:#{BRACKET_LINK_RE}|.)*?):/o
275
+ if m = re.match(line)
276
+ return m[1], m.post_match
277
+ else
278
+ return line, ""
279
+ end
280
+ end
281
+
282
+ TABLE_RE = /\A\|\|/
283
+
284
+ def compile_table(f)
285
+ lines = []
286
+ f.while_match(TABLE_RE) do |line|
287
+ lines.push line
288
+ skip_comments f
289
+ end
290
+ @output.table_open
291
+ lines.each do |line|
292
+ @output.table_record_open
293
+ split_columns(line.sub(TABLE_RE, "")).each do |col|
294
+ mid = col.sub!(/\A!/, "") ? "table_head" : "table_data"
295
+ span = col.slice!(/\A[\^>]*/)
296
+ rs = span_count(span, "^")
297
+ cs = span_count(span, ">")
298
+ @output.__send__(mid, compile_inline(col), rs, cs)
299
+ end
300
+ @output.table_record_close
301
+ end
302
+ @output.table_close
303
+ end
304
+
305
+ def split_columns(str)
306
+ cols = str.split(/\|\|/)
307
+ cols.pop if cols.last.chomp.empty?
308
+ cols
309
+ end
310
+
311
+ def span_count(str, ch)
312
+ c = str.count(ch)
313
+ c == 0 ? nil : c + 1
314
+ end
315
+
316
+ BLOCKQUOTE_RE = /\A""[ \t]?/
317
+
318
+ def compile_blockquote(f)
319
+ @output.blockquote_open
320
+ lines = []
321
+ f.while_match(BLOCKQUOTE_RE) do |line|
322
+ lines.push line.sub(BLOCKQUOTE_RE, "")
323
+ skip_comments f
324
+ end
325
+ compile_blocks lines.join("")
326
+ @output.blockquote_close
327
+ end
328
+
329
+ INDENTED_PRE_RE = /\A[ \t]/
330
+
331
+ def compile_indented_pre(f)
332
+ lines = f.span(INDENTED_PRE_RE)\
333
+ .map {|line| rstrip(line.sub(INDENTED_PRE_RE, "")) }\
334
+ .map {|line| @output.text(line) }
335
+ @output.preformatted restore_plugin_block(lines.join("\n"))
336
+ end
337
+
338
+ BLOCK_PRE_OPEN_RE = /\A<<<\s*(\w+)?/
339
+ BLOCK_PRE_CLOSE_RE = /\A>>>/
340
+
341
+ def compile_block_pre(f)
342
+ m = BLOCK_PRE_OPEN_RE.match(f.gets) or raise UnexpectedError, "must not happen"
343
+ str = restore_plugin_block(f.break(BLOCK_PRE_CLOSE_RE).join.chomp)
344
+ f.gets
345
+ @output.block_preformatted(str, m[1])
346
+ end
347
+
348
+ BLANK = /\A$/
349
+ PARAGRAPH_END_RE = Regexp.union(BLANK,
350
+ HEADER_RE, HRULE_RE, LIST_RE, DLIST_RE,
351
+ BLOCKQUOTE_RE, TABLE_RE,
352
+ INDENTED_PRE_RE, BLOCK_PRE_OPEN_RE)
353
+
354
+ def compile_paragraph(f)
355
+ lines = f.break(PARAGRAPH_END_RE)\
356
+ .reject {|line| COMMENT_RE =~ line }
357
+ if lines.size == 1 and /\A\0(\d+)\0\z/ =~ strip(lines[0])
358
+ @output.block_plugin plugin_block($1.to_i)
359
+ else
360
+ line_buffer = @output.container(:paragraph)
361
+ lines.each_with_index do |line, i|
362
+ buffer = @output.container
363
+ line_buffer << buffer
364
+ compile_inline(lstrip(line).chomp, buffer)
365
+ end
366
+ @output.paragraph(line_buffer)
367
+ end
368
+ end
369
+
370
+ #
371
+ # Inline Level
372
+ #
373
+
374
+ BRACKET_LINK_RE = /\[\[.+?\]\]/
375
+ URI_RE = /(?:https?|ftp|file|mailto):[A-Za-z0-9;\/?:@&=+$,\-_.!~*\'()#%]+/
376
+ WIKI_NAME_RE = /\b(?:[A-Z]+[a-z\d]+){2,}\b/
377
+
378
+ def inline_syntax_re
379
+ if @options[:use_wiki_name]
380
+ if @options[:use_not_wiki_name]
381
+ / (#{BRACKET_LINK_RE})
382
+ | (#{URI_RE})
383
+ | (#{MODIFIER_RE})
384
+ | (\^?#{WIKI_NAME_RE})
385
+ /xo
386
+ else
387
+ / (#{BRACKET_LINK_RE})
388
+ | (#{URI_RE})
389
+ | (#{MODIFIER_RE})
390
+ | (#{WIKI_NAME_RE})
391
+ /xo
392
+ end
393
+ else
394
+ / (#{BRACKET_LINK_RE})
395
+ | (#{URI_RE})
396
+ | (#{MODIFIER_RE})
397
+ /xo
398
+ end
399
+ end
400
+
401
+ def compile_inline(str, buf = nil)
402
+ buf ||= @output.container
403
+ re = inline_syntax_re
404
+ pending_str = nil
405
+ while m = re.match(str)
406
+ str = m.post_match
407
+
408
+ link, uri, mod, wiki_name = m[1, 4]
409
+ if wiki_name and wiki_name[0, 1] == "^"
410
+ pending_str = m.pre_match + wiki_name[1..-1]
411
+ next
412
+ end
413
+
414
+ pre_str = "#{pending_str}#{m.pre_match}"
415
+ pending_str = nil
416
+ evaluate_plugin_block(pre_str, buf)
417
+ compile_inline_markup(buf, link, uri, mod, wiki_name)
418
+ end
419
+ evaluate_plugin_block(pending_str || str, buf)
420
+ buf
421
+ end
422
+
423
+ def compile_inline_markup(buf, link, uri, mod, wiki_name)
424
+ case
425
+ when link
426
+ buf << compile_bracket_link(link[2...-2])
427
+ when uri
428
+ buf << compile_uri_autolink(uri)
429
+ when mod
430
+ buf << compile_modifier(mod)
431
+ when wiki_name
432
+ buf << @output.wiki_name(wiki_name)
433
+ else
434
+ raise UnexpectedError, "must not happen"
435
+ end
436
+ end
437
+
438
+ def compile_bracket_link(link)
439
+ if m = /\A(?>[^|\\]+|\\.)*\|/.match(link)
440
+ title = m[0].chop
441
+ uri = m.post_match
442
+ fixed_uri = fix_uri(uri)
443
+ if can_image_link?(uri)
444
+ @output.image_hyperlink(fixed_uri, title)
445
+ else
446
+ @output.hyperlink(fixed_uri, compile_modifier(title))
447
+ end
448
+ else
449
+ fixed_link = fix_uri(link)
450
+ if can_image_link?(link)
451
+ @output.image_hyperlink(fixed_link)
452
+ else
453
+ @output.hyperlink_namedpage(fixed_link, @output.text(link))
454
+ end
455
+ end
456
+ end
457
+
458
+ def can_image_link?(uri)
459
+ image?(uri) and @options[:allow_bracket_inline_image]
460
+ end
461
+
462
+ def compile_uri_autolink(uri)
463
+ if image?(uri)
464
+ @output.image_hyperlink(fix_uri(uri))
465
+ else
466
+ @output.hyperlink(fix_uri(uri), @output.text(uri))
467
+ end
468
+ end
469
+
470
+ def fix_uri(uri)
471
+ if /\A(?:https?|ftp|file):(?!\/\/)/ =~ uri
472
+ uri.sub(/\A\w+:/, "")
473
+ else
474
+ uri
475
+ end
476
+ end
477
+
478
+ IMAGE_EXTS = %w(.jpg .jpeg .gif .png)
479
+
480
+ def image?(uri)
481
+ IMAGE_EXTS.include?(File.extname(uri).downcase)
482
+ end
483
+
484
+ STRONG = "'''"
485
+ EM = "''"
486
+ DEL = "=="
487
+
488
+ STRONG_RE = /'''.+?'''/
489
+ EM_RE = /''.+?''/
490
+ DEL_RE = /==.+?==/
491
+
492
+ MODIFIER_RE = Regexp.union(STRONG_RE, EM_RE, DEL_RE)
493
+
494
+ MODTAG = {
495
+ STRONG => "strong",
496
+ EM => "em",
497
+ DEL => "del"
498
+ }
499
+
500
+ def compile_modifier(str)
501
+ buf = @output.container
502
+ while m = / (#{MODIFIER_RE})
503
+ /xo.match(str)
504
+ evaluate_plugin_block(m.pre_match, buf)
505
+ case
506
+ when chunk = m[1]
507
+ mod, s = split_mod(chunk)
508
+ mid = MODTAG[mod]
509
+ buf << @output.__send__(mid, compile_inline(s))
510
+ else
511
+ raise UnexpectedError, "must not happen"
512
+ end
513
+ str = m.post_match
514
+ end
515
+ evaluate_plugin_block(str, buf)
516
+ buf
517
+ end
518
+
519
+ def split_mod(str)
520
+ case str
521
+ when /\A'''/
522
+ return str[0, 3], str[3...-3]
523
+ when /\A''/
524
+ return str[0, 2], str[2...-2]
525
+ when /\A==/
526
+ return str[0, 2], str[2...-2]
527
+ else
528
+ raise UnexpectedError, "must not happen: #{str.inspect}"
529
+ end
530
+ end
531
+
532
+ def strip(str)
533
+ rstrip(lstrip(str))
534
+ end
535
+
536
+ def rstrip(str)
537
+ str.sub(/[ \t\r\n\v\f]+\z/, "")
538
+ end
539
+
540
+ def lstrip(str)
541
+ str.sub(/\A[ \t\r\n\v\f]+/, "")
542
+ end
543
+
544
+
545
+ class HTMLOutput
546
+ def initialize(suffix = " />")
547
+ @suffix = suffix
548
+ @f = nil
549
+ end
550
+
551
+ def reset
552
+ @f = StringIO.new
553
+ end
554
+
555
+ def finish
556
+ @f.string
557
+ end
558
+
559
+ def container(_for=nil)
560
+ case _for
561
+ when :paragraph
562
+ []
563
+ else
564
+ ""
565
+ end
566
+ end
567
+
568
+ #
569
+ # Procedures
570
+ #
571
+
572
+ def headline(level, title)
573
+ @f.puts "<h#{level}>#{title}</h#{level}>"
574
+ end
575
+
576
+ def hrule
577
+ @f.puts "<hr#{@suffix}"
578
+ end
579
+
580
+ def list_begin
581
+ end
582
+
583
+ def list_end
584
+ @f.puts
585
+ end
586
+
587
+ def list_open(type)
588
+ @f.puts "<#{type}>"
589
+ end
590
+
591
+ def list_close(type)
592
+ @f.print "</#{type}>"
593
+ end
594
+
595
+ def listitem_open
596
+ @f.print "<li>"
597
+ end
598
+
599
+ def listitem_close
600
+ @f.puts "</li>"
601
+ end
602
+
603
+ def listitem(item)
604
+ @f.print item
605
+ end
606
+
607
+ def dlist_open
608
+ @f.puts "<dl>"
609
+ end
610
+
611
+ def dlist_close
612
+ @f.puts "</dl>"
613
+ end
614
+
615
+ def dlist_item(dt, dd)
616
+ case
617
+ when dd.empty?
618
+ @f.puts "<dt>#{dt}</dt>"
619
+ when dt.empty?
620
+ @f.puts "<dd>#{dd}</dd>"
621
+ else
622
+ @f.puts "<dt>#{dt}</dt>"
623
+ @f.puts "<dd>#{dd}</dd>"
624
+ end
625
+ end
626
+
627
+ def table_open
628
+ @f.puts %Q(<table border="1">)
629
+ end
630
+
631
+ def table_close
632
+ @f.puts "</table>"
633
+ end
634
+
635
+ def table_record_open
636
+ @f.print "<tr>"
637
+ end
638
+
639
+ def table_record_close
640
+ @f.puts "</tr>"
641
+ end
642
+
643
+ def table_head(item, rs, cs)
644
+ @f.print "<th#{tdattr(rs, cs)}>#{item}</th>"
645
+ end
646
+
647
+ def table_data(item, rs, cs)
648
+ @f.print "<td#{tdattr(rs, cs)}>#{item}</td>"
649
+ end
650
+
651
+ def tdattr(rs, cs)
652
+ buf = ""
653
+ buf << %Q( rowspan="#{rs}") if rs
654
+ buf << %Q( colspan="#{cs}") if cs
655
+ buf
656
+ end
657
+ private :tdattr
658
+
659
+ def blockquote_open
660
+ @f.print "<blockquote>"
661
+ end
662
+
663
+ def blockquote_close
664
+ @f.puts "</blockquote>"
665
+ end
666
+
667
+ def block_preformatted(str, info)
668
+ syntax = info ? info.downcase : nil
669
+ if syntax
670
+ begin
671
+ convertor = Syntax::Convertors::HTML.for_syntax(syntax)
672
+ @f.puts convertor.convert(str)
673
+ return
674
+ rescue NameError, RuntimeError
675
+ end
676
+ end
677
+ preformatted(text(str))
678
+ end
679
+
680
+ def preformatted(str)
681
+ @f.print "<pre>"
682
+ @f.print str
683
+ @f.puts "</pre>"
684
+ end
685
+
686
+ def paragraph(lines)
687
+ @f.puts "<p>#{lines.join("\n")}</p>"
688
+ end
689
+
690
+ def block_plugin(str)
691
+ @f.puts %Q(<div class="plugin">{{#{escape_html(str)}}}</div>)
692
+ end
693
+
694
+ #
695
+ # Functions
696
+ #
697
+
698
+ def hyperlink(uri, title)
699
+ %Q(<a href="#{escape_html_param(uri)}">#{title}</a>)
700
+ end
701
+
702
+ def hyperlink_namedpage(uri, title)
703
+ %Q(<a href="#{escape_html_param(uri)}">#{title}</a>)
704
+ end
705
+
706
+ def wiki_name(name)
707
+ hyperlink_namedpage(name, text(name))
708
+ end
709
+
710
+ def image_hyperlink(uri, alt = nil)
711
+ alt ||= uri.split(/\//).last
712
+ alt = escape_html(alt)
713
+ %Q(<img src="#{escape_html_param(uri)}" alt="#{alt}"#{@suffix})
714
+ end
715
+
716
+ def strong(item)
717
+ "<strong>#{item}</strong>"
718
+ end
719
+
720
+ def em(item)
721
+ "<em>#{item}</em>"
722
+ end
723
+
724
+ def del(item)
725
+ "<del>#{item}</del>"
726
+ end
727
+
728
+ def text(str)
729
+ escape_html(str)
730
+ end
731
+
732
+ def inline_plugin(src)
733
+ %Q(<span class="plugin">{{#{src}}}</span>)
734
+ end
735
+
736
+ #
737
+ # Utilities
738
+ #
739
+
740
+ def escape_html_param(str)
741
+ escape_quote(escape_html(str))
742
+ end
743
+
744
+ def escape_html(text)
745
+ text.gsub(/&/, "&amp;").gsub(/</, "&lt;").gsub(/>/, "&gt;")
746
+ end
747
+
748
+ def unescape_html(text)
749
+ text.gsub(/&gt;/, ">").gsub(/&lt;/, "<").gsub(/&amp;/, "&")
750
+ end
751
+
752
+ def escape_quote(text)
753
+ text.gsub(/"/, "&quot;")
754
+ end
755
+ end
756
+
757
+
758
+ class LineInput
759
+ def initialize(f)
760
+ @input = f
761
+ @buf = []
762
+ @lineno = 0
763
+ @eof_p = false
764
+ end
765
+
766
+ def inspect
767
+ "\#<#{self.class} file=#{@input.inspect} line=#{lineno()}>"
768
+ end
769
+
770
+ def eof?
771
+ @eof_p
772
+ end
773
+
774
+ def lineno
775
+ @lineno
776
+ end
777
+
778
+ def gets
779
+ unless @buf.empty?
780
+ @lineno += 1
781
+ return @buf.pop
782
+ end
783
+ return nil if @eof_p # to avoid ARGF blocking.
784
+ line = @input.gets
785
+ line = line.sub(/\r\n/, "\n") if line
786
+ @eof_p = line.nil?
787
+ @lineno += 1
788
+ line
789
+ end
790
+
791
+ def ungets(line)
792
+ return unless line
793
+ @lineno -= 1
794
+ @buf.push line
795
+ line
796
+ end
797
+
798
+ def peek
799
+ line = gets()
800
+ ungets line if line
801
+ line
802
+ end
803
+
804
+ def next?
805
+ peek() ? true : false
806
+ end
807
+
808
+ def skip_blank_lines
809
+ n = 0
810
+ while line = gets()
811
+ unless line.strip.empty?
812
+ ungets line
813
+ return n
814
+ end
815
+ n += 1
816
+ end
817
+ n
818
+ end
819
+
820
+ def gets_if(re)
821
+ line = gets()
822
+ if not line or not (re =~ line)
823
+ ungets line
824
+ return nil
825
+ end
826
+ line
827
+ end
828
+
829
+ def gets_unless(re)
830
+ line = gets()
831
+ if not line or re =~ line
832
+ ungets line
833
+ return nil
834
+ end
835
+ line
836
+ end
837
+
838
+ def each
839
+ while line = gets()
840
+ yield line
841
+ end
842
+ end
843
+
844
+ def while_match(re)
845
+ while line = gets()
846
+ unless re =~ line
847
+ ungets line
848
+ return
849
+ end
850
+ yield line
851
+ end
852
+ nil
853
+ end
854
+
855
+ def getlines_while(re)
856
+ buf = []
857
+ while_match(re) do |line|
858
+ buf.push line
859
+ end
860
+ buf
861
+ end
862
+
863
+ alias span getlines_while # from Haskell
864
+
865
+ def until_match(re)
866
+ while line = gets()
867
+ if re =~ line
868
+ ungets line
869
+ return
870
+ end
871
+ yield line
872
+ end
873
+ nil
874
+ end
875
+
876
+ def getlines_until(re)
877
+ buf = []
878
+ until_match(re) do |line|
879
+ buf.push line
880
+ end
881
+ buf
882
+ end
883
+
884
+ alias break getlines_until # from Haskell
885
+
886
+ def until_terminator(re)
887
+ while line = gets()
888
+ return if re =~ line # discard terminal line
889
+ yield line
890
+ end
891
+ nil
892
+ end
893
+
894
+ def getblock(term_re)
895
+ buf = []
896
+ until_terminator(term_re) do |line|
897
+ buf.push line
898
+ end
899
+ buf
900
+ end
901
+ end
902
+ end
903
+
904
+ if __FILE__ == $0
905
+ puts HikiDoc.to_html(ARGF.read(nil))
906
+ end
@@ -0,0 +1,8 @@
1
+ require 'uri'
2
+ class MixiStyle
3
+ def self.to_html(text)
4
+ html = text.sub(/\A(.+)\n+/) { "<h2>#{$1}</h2>" }.
5
+ gsub(URI.regexp(%w(http https ftp mailto))) {|m| %[<a href="#{m}">#{m}</a>] }.
6
+ gsub("\n", "<br />\n")
7
+ end
8
+ end
@@ -0,0 +1,57 @@
1
+ require 'pipin'
2
+ require 'rake/clean'
3
+
4
+ def dst_html(label)
5
+ File.join DSTDIR, "#{label}.html"
6
+ end
7
+
8
+ SRCDIR = 'data'
9
+ DSTDIR = 'public'
10
+ SRC_EXTNAMES = %w(html txt)
11
+ EXTS = SRC_EXTNAMES.join(',')
12
+
13
+ DIARY_SRCS = FileList["#{SRCDIR}/#{Pipin::Diary_pattern}.{#{EXTS}}"]
14
+ SRCS = FileList["#{SRCDIR}/#{Pipin::Post_pattern}.{#{EXTS}}"]
15
+ DSTS = SRCS.pathmap("#{DSTDIR}/%n.html")
16
+ MONTH_DSTS = Pipin::Post.year_months.map {|year, months|
17
+ months.map {|month| dst_html(year + month) }
18
+ }.flatten
19
+
20
+ task :default => [:index, :archives, :months, :sitemap] + DSTS.sort.reverse
21
+
22
+ # posts
23
+ SRC_EXTNAMES.each do |extname|
24
+ rule /public\/\w+\.html/ => "#{SRCDIR}/%n.#{extname}" do |t|
25
+ Pipin.build 'post', File.basename(t.name, '.html')
26
+ end
27
+ end
28
+
29
+ # mouths
30
+ task :months => MONTH_DSTS
31
+ Pipin::Post.year_months.each do |year, months|
32
+ months.each do |month|
33
+ dst = dst_html(year + month)
34
+ srcs = FileList["#{SRCDIR}/#{year + month}[0-9][0-9]*.{#{EXTS}}"]
35
+ file dst => srcs do
36
+ Pipin.build 'month', year, month
37
+ end
38
+ end
39
+ end
40
+
41
+ # other
42
+ OTHER_TASKS ={
43
+ 'index' => DIARY_SRCS.sort.reverse[0, 3],
44
+ 'archives' => DIARY_SRCS,
45
+ 'sitemap' => SRCS,
46
+ }
47
+ OTHER_TASKS.each do |pagename, srcs|
48
+ dst = dst_html(pagename)
49
+ task pagename => dst
50
+ file dst => srcs do
51
+ Pipin.build pagename
52
+ end
53
+ end
54
+
55
+ # clean
56
+ OTHER_DSTS = OTHER_TASKS.keys.map {|page| dst_html(page) }
57
+ CLEAN.include(DSTS, MONTH_DSTS, OTHER_DSTS)
@@ -0,0 +1,3 @@
1
+ #$LOAD_PATH.unshift '../lib'
2
+ require 'rubygems'
3
+ require 'pipin/build_tasks'
@@ -0,0 +1,56 @@
1
+ # -*- encoding: UTF-8 -*-
2
+ def root_path
3
+ File.dirname(File.expand_path(__FILE__))
4
+ end
5
+
6
+ # 各種パラメータ設定
7
+ def config
8
+ {
9
+ # ブログのURLに書き換えてください
10
+ #:base_url => 'http://dgames.jp/pipin', # for pipinrss
11
+ :title => 'Thie Pipin Diary',
12
+ #:description => 'Please set config[:description] in pipinrc',
13
+ :dir => {
14
+ #:posts => '/Users/dan/w/piping/datasample',
15
+ #:views => '/Users/dan/w/piping/views',
16
+ #:plugins => "#{root_path}/config/plugins",
17
+ },
18
+ :limit => 5,
19
+ # 複数のcssを使用する場合は配列で指定してください
20
+ :css => '/stylesheets/pipin.css',
21
+ #:css => '/stylesheets/nobunaga/nobunaga.css',
22
+ }
23
+ end
24
+
25
+ # 記事のコンパイラを設定
26
+ def add_compilers
27
+ # HikiDocを登録(拡張「.txt」に関連付ける)
28
+ require 'misc/hikidoc.rb'
29
+ Pipin::Post.add_compiler('.txt') {|post|
30
+ HikiDoc.to_html(post.body, :level => 2)
31
+ # 記事にpluginを埋め込むなら以下を使う
32
+ # 記事に書く例: {{amazon '4777512924'}}
33
+ #eval_hiki_plugin(HikiDoc.to_html(post.body, :level => 2))
34
+ }
35
+
36
+ # mixiスタイルを登録
37
+ require 'misc/mixistyle.rb'
38
+ Pipin::Post.add_compiler('.mixi') {|post|
39
+ MixiStyle.to_html(post.body)
40
+ }
41
+
42
+ # 拡張子「.src」のソースをそのまま表示する例
43
+ Pipin::Post.add_compiler('.src') {|post|
44
+ "<h2>#{h post.filename}</h2><pre>#{h post.body}</pre>"
45
+ }
46
+ end
47
+
48
+ def setup_environment
49
+ if defined? Encoding
50
+ Encoding.default_external = 'UTF-8' # 1.9
51
+ else
52
+ $KCODE = 'UTF-8' # 1.8
53
+ end
54
+ add_compilers
55
+ #Pipin::Post.posts_dir = config[:dir][:posts]
56
+ end
data/lib/pipin/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pipin
2
- VERSION = "0.0.3"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,5 @@
1
+ %h2 archives
2
+ - years.each do |year, months|
3
+ %h3&= year
4
+ - months.each do |month|
5
+ %a{:href => "#{year}#{month}#{htmlsufix}"}&= month
@@ -6,10 +6,14 @@
6
6
  %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
7
7
  %meta{:content => "text/javascript", "http-equiv" => "Content-Script-Type"}/
8
8
  %meta{:category => "ruby; setup"}/
9
- - css = 'pipin.css' || config[:css]
10
- %link{:rel => "stylesheet", :type => 'text/css', :href => css}/
11
- %title= @title# || config[:title]
9
+ %link{:rel => "stylesheet", :type => 'text/css', :href => 'pipin.css' || config[:css]}/
10
+ %title&= @title || config[:title]
12
11
  %body
12
+ %h1&= config[:title]
13
+ .menus
14
+ %a{:href => './index' + htmlsufix} Latest
15
+ %a{:href => './archives' + htmlsufix} Archives
16
+ %a{:href => './about' + htmlsufix} About
13
17
  .main
14
18
  =# render_plugin_hook(:flavour_header, self)
15
19
  .content
@@ -17,3 +21,11 @@
17
21
  != yield
18
22
  .before_content=# render_plugin_hook(:after_content, self)
19
23
  =# render_plugin_hook(:flavour_footer, self)
24
+ .footer
25
+ .sitemap
26
+ %a{:href => './sitemap.html'} Sitemap
27
+ %a{:href => 'https://github.com/dan5/pipin'} Pipin
28
+ &= Pipin::VERSION
29
+ on
30
+ %a{:href => 'http://www.ruby-lang.org/'} Ruby
31
+ &= RUBY_VERSION
@@ -0,0 +1,8 @@
1
+ .before_entries
2
+ - posts.each do |post|
3
+ .post
4
+ .date 2000-00-00
5
+ .body~ post.to_html
6
+ .permalink
7
+ %a{:href => post.label + htmlsufix} permalink
8
+ .after_entries
@@ -0,0 +1,7 @@
1
+ .post
2
+ .before_post
3
+ .date 2000-00-00
4
+ .body~ post.to_html
5
+ .permalink
6
+ %a{:href => post.label + htmlsufix} permalink
7
+ .after_post
@@ -0,0 +1,5 @@
1
+ %h2 sitemap
2
+ %ul
3
+ - posts.each do |post|
4
+ %li
5
+ %a{:href => post.label + htmlsufix}&= post.label
data/lib/pipin.rb CHANGED
@@ -2,40 +2,76 @@ require 'pipin/version'
2
2
  require 'haml'
3
3
  require 'fileutils'
4
4
 
5
- include Haml::Helpers
6
- alias h html_escape
7
-
8
- def rootdir
9
- File.join(File.dirname(File.expand_path(__FILE__)).untaint, '..')
10
- end
5
+ def rootdir() File.join(File.dirname(File.expand_path(__FILE__)), 'pipin') end
6
+ def htmlsufix() '.html' end
11
7
 
12
8
  module Pipin
13
- def self.command(cmd)
14
- __send__ "command_#{cmd}", *ARGV
9
+ Diary_pattern = '[0-9]' * 8 + '*'
10
+ Post_pattern = '[0-9a-zA-Z]*'
11
+
12
+ def self.build(page_name, *args)
13
+ command :build, page_name, *args
14
+ end
15
+
16
+ def self.command(cmd, *args)
17
+ opts = args.empty? ? ARGV : args
18
+ __send__ "command_#{cmd}", *opts
15
19
  end
16
20
 
17
21
  def self.command_new(dir)
18
22
  File.exist?(dir) && raise("File exists - #{dir}")
19
- FileUtils.cp_r File.join(rootdir ,'templatefiles'), dir
23
+ FileUtils.cp_r File.join(rootdir ,'templates'), dir
20
24
  puts Dir.glob(File.join(dir, '**/*')).map {|e| ' create ' + e }
21
25
  end
22
26
 
23
- def self.command_build(dir = '.')
24
- load dir + '/pipinrc'
25
- build = Builder.new('public')
26
- build.render_posts
27
- build.render_list('index', '20*', :limit => 3)
27
+ def self.command_build(page_name, *args)
28
+ Builder.new('public').__send__('render_' + page_name, *args)
28
29
  end
29
30
 
30
31
  class Builder
31
- def initialize(distdir)
32
- @distdir = distdir
32
+ def initialize(dist_dir)
33
+ @dist_dir = dist_dir
34
+ load './pipinrc'
35
+ setup_environment
33
36
  end
34
37
 
35
- def render_posts
36
- Post.find('20*').each do |post|
37
- write_html post.label, render_with_layout(:post, binding)
38
- end
38
+ def render_sitemap
39
+ posts = Post.find(Post_pattern)
40
+ write_html 'sitemap', render_with_layout(:sitemap, binding)
41
+ end
42
+
43
+ def render_archives
44
+ years = Post.year_months
45
+ write_html 'archives', render_with_layout(:archives, binding)
46
+ end
47
+
48
+ #def render_months
49
+ # years = Post.year_months
50
+ # years.each do |year, months|
51
+ # months.each do |month|
52
+ # render_month(year, month)
53
+ # end
54
+ # end
55
+ #end
56
+
57
+ def render_month(year, month)
58
+ name = year + month
59
+ render_list(name, name + '*')
60
+ end
61
+
62
+ #def render_posts
63
+ # Post.find(Post_pattern).each do |post|
64
+ # write_html post.label, render_with_layout(:post, binding)
65
+ # end
66
+ #end
67
+
68
+ def render_post(label)
69
+ post = Post.find(label).first
70
+ write_html label, render_with_layout(:post, binding)
71
+ end
72
+
73
+ def render_index
74
+ render_list('index', Diary_pattern, :limit => 3)
39
75
  end
40
76
 
41
77
  def render_list(name, pattern, options = {})
@@ -44,8 +80,10 @@ module Pipin
44
80
  end
45
81
 
46
82
  def write_html(label, html)
47
- Dir.mkdir @distdir unless File.exist?(@distdir)
48
- File.open(File.join(@distdir, label + '.html'), 'w') {|f| f.write html }
83
+ Dir.mkdir @dist_dir unless File.exist?(@dist_dir)
84
+ filename = File.join(@dist_dir, label + '.html')
85
+ File.open(filename, 'w') {|f| f.write html }
86
+ puts ' create ' + filename
49
87
  end
50
88
 
51
89
  def render_with_layout(template, b = binding)
@@ -59,18 +97,42 @@ module Pipin
59
97
  end
60
98
 
61
99
  class Post
62
- @@entries_dir = 'datasample'
63
100
  def self.find(pattern, options = {})
64
- files = Dir.chdir(@@entries_dir) { Dir.glob(pattern) }.sort.reverse
65
- files = files[0, options[:limit]] if options[:limit]
66
- files.map {|e| Post.new(e) }
101
+ self.find_srcs(pattern, options).map {|e| Post.new(e) }
102
+ end
103
+
104
+ def self.find_srcs(pattern, options = {})
105
+ files = Dir.chdir(@@posts_dir) { Dir.glob(pattern + '.{txt,html}') }.sort.reverse
106
+ options[:limit] ? files[0, options[:limit]] : files
107
+ end
108
+
109
+ def self.year_months
110
+ result = {}
111
+ files = find_srcs(Diary_pattern)
112
+ first_year = files.last[/^\d{4}/]
113
+ last_year = files.first[/^\d{4}/]
114
+ (first_year..last_year).to_a.reverse.map do |year|
115
+ result[year] = ('01'..'12').select {|month| find_srcs("#{year}#{month}*")[0] }
116
+ end
117
+ result
118
+ end
119
+
120
+ @@compilers = [[nil, lambda {|post| post.body }]]
121
+ def self.add_compiler(extname = nil, &block)
122
+ @@compilers.unshift [extname, block]
123
+ end
124
+
125
+ @@posts_dir = 'data'
126
+ def self.posts_dir=(dir)
127
+ @@posts_dir = dir
67
128
  end
68
129
 
69
130
  attr_reader :filename, :header, :body
70
131
  def initialize(filename)
71
132
  @filename = filename
72
- @header, @body = Dir.chdir(@@entries_dir) { File.read(@filename) }.split(/^__$/, 2)
133
+ @header, @body = Dir.chdir(@@posts_dir) { File.read(@filename) }.split(/^__$/, 2)
73
134
  @header, @body = nil, @header unless @body
135
+ @header and p(@header)
74
136
  end
75
137
 
76
138
  def label
@@ -78,7 +140,9 @@ module Pipin
78
140
  end
79
141
 
80
142
  def to_html
81
- body
143
+ (@@compilers.assoc(File.extname filename) || @@compilers.last)[1].call(self)
144
+ rescue
145
+ Haml::Helpers::html_escape $!
82
146
  end
83
147
  end
84
148
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: pipin
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.3
5
+ version: 0.1.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - dan5ya
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-06-22 00:00:00 +09:00
13
+ date: 2011-06-23 00:00:00 +09:00
14
14
  default_executable:
15
15
  dependencies: []
16
16
 
@@ -28,17 +28,24 @@ files:
28
28
  - Gemfile
29
29
  - README.rdoc
30
30
  - Rakefile
31
+ - lib/misc/hikidoc.rb
32
+ - lib/misc/mixistyle.rb
31
33
  - lib/pipin.rb
34
+ - lib/pipin/build_tasks.rb
35
+ - lib/pipin/templates/Rakefile
36
+ - lib/pipin/templates/data/20110620.txt
37
+ - lib/pipin/templates/data/20110622.txt
38
+ - lib/pipin/templates/data/about.txt
39
+ - lib/pipin/templates/data/hello.src
40
+ - lib/pipin/templates/pipinrc
41
+ - lib/pipin/templates/stylesheets/pipin.css
32
42
  - lib/pipin/version.rb
43
+ - lib/pipin/views/archives.haml
44
+ - lib/pipin/views/layout.haml
45
+ - lib/pipin/views/list.haml
46
+ - lib/pipin/views/post.haml
47
+ - lib/pipin/views/sitemap.haml
33
48
  - pipin.gemspec
34
- - templatefiles/datasample/20110620.txt
35
- - templatefiles/datasample/20110622.txt
36
- - templatefiles/datasample/about.txt
37
- - templatefiles/datasample/hello.src
38
- - templatefiles/pipinrc
39
- - views/layout.haml
40
- - views/list.haml
41
- - views/post.haml
42
49
  has_rdoc: true
43
50
  homepage: https://github.com/dan5/pipin
44
51
  licenses: []
data/views/list.haml DELETED
@@ -1,4 +0,0 @@
1
- .before_entries=# render_plugin_hook(:before_entries, self, @entries)
2
- - posts.each do |post|
3
- ~ render(:post, binding)
4
- .after_entries=# render_plugin_hook(:after_entries, self, @entries)
data/views/post.haml DELETED
@@ -1,4 +0,0 @@
1
- .post
2
- .before_post=# render_plugin_hook(:before_entry, @entry)
3
- .body~ post.to_html
4
- .after_post=# render_plugin_hook(:after_entry, @entry, self)