pipin 0.0.3 → 0.1.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/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)