hobix 0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/hobix +90 -0
- data/lib/hobix/api.rb +91 -0
- data/lib/hobix/article.rb +22 -0
- data/lib/hobix/base.rb +477 -0
- data/lib/hobix/bixwik.rb +200 -0
- data/lib/hobix/commandline.rb +661 -0
- data/lib/hobix/comments.rb +99 -0
- data/lib/hobix/config.rb +39 -0
- data/lib/hobix/datamarsh.rb +110 -0
- data/lib/hobix/entry.rb +83 -0
- data/lib/hobix/facets/comments.rb +74 -0
- data/lib/hobix/facets/publisher.rb +314 -0
- data/lib/hobix/facets/trackbacks.rb +80 -0
- data/lib/hobix/linklist.rb +76 -0
- data/lib/hobix/out/atom.rb +92 -0
- data/lib/hobix/out/erb.rb +64 -0
- data/lib/hobix/out/okaynews.rb +55 -0
- data/lib/hobix/out/quick.rb +312 -0
- data/lib/hobix/out/rdf.rb +97 -0
- data/lib/hobix/out/redrum.rb +26 -0
- data/lib/hobix/out/rss.rb +115 -0
- data/lib/hobix/plugin/bloglines.rb +73 -0
- data/lib/hobix/plugin/calendar.rb +220 -0
- data/lib/hobix/plugin/flickr.rb +110 -0
- data/lib/hobix/plugin/recent_comments.rb +82 -0
- data/lib/hobix/plugin/sections.rb +91 -0
- data/lib/hobix/plugin/tags.rb +60 -0
- data/lib/hobix/publish/ping.rb +53 -0
- data/lib/hobix/publish/replicate.rb +283 -0
- data/lib/hobix/publisher.rb +18 -0
- data/lib/hobix/search/dictionary.rb +141 -0
- data/lib/hobix/search/porter_stemmer.rb +203 -0
- data/lib/hobix/search/simple.rb +209 -0
- data/lib/hobix/search/vector.rb +100 -0
- data/lib/hobix/storage/filesys.rb +398 -0
- data/lib/hobix/trackbacks.rb +94 -0
- data/lib/hobix/util/objedit.rb +193 -0
- data/lib/hobix/util/patcher.rb +155 -0
- data/lib/hobix/webapp/cli.rb +195 -0
- data/lib/hobix/webapp/htmlform.rb +107 -0
- data/lib/hobix/webapp/message.rb +177 -0
- data/lib/hobix/webapp/urigen.rb +141 -0
- data/lib/hobix/webapp/webrick-servlet.rb +90 -0
- data/lib/hobix/webapp.rb +723 -0
- data/lib/hobix/weblog.rb +860 -0
- data/lib/hobix.rb +223 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b0a7b407b2f990570b493a45f6992f4e3ea0adb8d8d57c7655da7fff8187ce8e
|
4
|
+
data.tar.gz: 30b0d1831163f5b6623f63cc45bf09f539efa19af2d4f22e3437eb9251fd15c2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8f9e25975d03aa9b75ab92a74b8ff6a05e28c2b0b025ba51a1bbb0c2607a689f9fd97b702d9abf69b079a6e61e1b87ce0e2f9039950b8619a7e590ab599fd9c9
|
7
|
+
data.tar.gz: b9d521d17ce43bbc1618490fef2c0a37aa825bfa22df1922f554e832bdb7f4e56b2b0803a37bb94b827d352fae5b29d93b4907028d77bc65e4bcd9f4f241ecdb
|
data/bin/hobix
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'hobix/commandline'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
cmdline = Class.new
|
6
|
+
cmdline.extend Hobix::CommandLine
|
7
|
+
|
8
|
+
def print_usage( cmdline )
|
9
|
+
puts "hobix #{ Hobix::VERSION } on ruby #{ ::RUBY_VERSION } (#{ ::RUBY_RELEASE_DATE }) [#{ ::RUBY_PLATFORM }]"
|
10
|
+
puts "Usage: hobix command weblog-name [command-options]"
|
11
|
+
puts "Commands are"
|
12
|
+
['app', 'weblog', 'action'].each do |cmd_type|
|
13
|
+
cmdline.methods.collect do |m|
|
14
|
+
if m =~ /^(\w+)_(#{ cmd_type })$/
|
15
|
+
[$1, $&]
|
16
|
+
end
|
17
|
+
end.compact.sort.each do |cmd, m|
|
18
|
+
args = "#{ cmd } #{ cmdline.method( "#{ m }_args" ).call.join( ' ' ) }"
|
19
|
+
exp = cmdline.method( "#{ m }_explain" ).call.gsub( /\n/, "\n" + ( " " * 40 ) )
|
20
|
+
puts " %-38s%-40s" % [args, exp]
|
21
|
+
end
|
22
|
+
puts
|
23
|
+
end
|
24
|
+
exit
|
25
|
+
end
|
26
|
+
|
27
|
+
config = nil
|
28
|
+
opts = OptionParser.new
|
29
|
+
opts.on( "-c", "--config FILE", String ) { |val| config = val }
|
30
|
+
|
31
|
+
args = opts.parse( *ARGV )
|
32
|
+
print_usage( cmdline ) if args.length < 1 or args[0] == 'help'
|
33
|
+
|
34
|
+
# unfreeze the arguments
|
35
|
+
cmd, *opts = args.collect do |arg|
|
36
|
+
arg.dup
|
37
|
+
end
|
38
|
+
args.clear
|
39
|
+
|
40
|
+
cmdline.login( config )
|
41
|
+
|
42
|
+
mname = nil
|
43
|
+
if cmd == 'setup_blogs'
|
44
|
+
mname = cmd
|
45
|
+
elsif cmdline.respond_to? "#{ cmd }_app"
|
46
|
+
mname = "#{ cmd }_app"; opts = [YAML::load( DATA )]
|
47
|
+
elsif cmdline.respond_to? "#{ cmd }_weblog"
|
48
|
+
mname = "#{ cmd }_weblog"
|
49
|
+
elsif cmdline.respond_to? "#{ cmd }_action"
|
50
|
+
weblog = opts.shift
|
51
|
+
unless cmdline.config['weblogs'].respond_to? :has_key?
|
52
|
+
puts "*** no weblogs found in your configuration."
|
53
|
+
puts "*** use `hobix create' or `hobix add' to setup."
|
54
|
+
exit
|
55
|
+
end
|
56
|
+
unless cmdline.config['weblogs'].has_key? weblog
|
57
|
+
puts "*** no weblog `#{ weblog }' found in your configuration."
|
58
|
+
puts "*** type `hobix help' and check your spelling."
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
hobix_weblog = URI::parse( cmdline.config['weblogs'][ weblog ] )
|
62
|
+
if hobix_weblog.scheme
|
63
|
+
mname = "#{ hobix_weblog.scheme }_#{ cmd }_remote"
|
64
|
+
unless cmdline.respond_to? mname
|
65
|
+
opts.unshift cmd
|
66
|
+
mname = hobix_weblog.scheme
|
67
|
+
end
|
68
|
+
else
|
69
|
+
hobix_weblog = Hobix::Weblog.load( cmdline.config['weblogs'][ weblog ] )
|
70
|
+
mname = "#{ cmd }_action"
|
71
|
+
end
|
72
|
+
opts.unshift hobix_weblog
|
73
|
+
end
|
74
|
+
unless mname
|
75
|
+
abort "*** no hobix command `#{ cmd }'. use `hobix' without arguments to get help."
|
76
|
+
end
|
77
|
+
m = cmdline.method( mname )
|
78
|
+
if m.arity == opts.length or (m.arity < 0 and opts.length >= m.arity.abs - 1)
|
79
|
+
m.call( *opts )
|
80
|
+
else
|
81
|
+
arglist = [cmd] + cmdline.method( "#{ mname }_args" ).call
|
82
|
+
need = m.arity
|
83
|
+
need = need.abs - 1 if need < 0
|
84
|
+
puts "*** wrong arguments (#{opts.length} given, #{need} needed)"
|
85
|
+
puts "*** use syntax: `hobix #{ arglist.join( ' ' ) }'"
|
86
|
+
end
|
87
|
+
|
88
|
+
__END__
|
89
|
+
# configuration
|
90
|
+
--- {}
|
data/lib/hobix/api.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
#
|
2
|
+
# = hobix/api.rb
|
3
|
+
#
|
4
|
+
# Hobix API, used by any external service (DRb or REST, etc.)
|
5
|
+
#
|
6
|
+
# Copyright (c) 2003-2004 why the lucky stiff
|
7
|
+
#
|
8
|
+
# Written & maintained by why the lucky stiff <why@ruby-lang.org>
|
9
|
+
#
|
10
|
+
# This program is free software, released under a BSD license.
|
11
|
+
# See COPYING for details.
|
12
|
+
#
|
13
|
+
#--
|
14
|
+
# $Id$
|
15
|
+
#++
|
16
|
+
module Hobix
|
17
|
+
|
18
|
+
# The API facet
|
19
|
+
class API < BaseFacet
|
20
|
+
|
21
|
+
def initialize( weblog, defaults = {} )
|
22
|
+
@weblog = weblog
|
23
|
+
end
|
24
|
+
def get app
|
25
|
+
if app.respond_to? :action_uri
|
26
|
+
return true unless protect app, @weblog
|
27
|
+
@app = app
|
28
|
+
prefix, action, *args = app.action_uri.split( '/' )
|
29
|
+
if prefix == "remote"
|
30
|
+
if respond_to? "#{ action }_action"
|
31
|
+
begin
|
32
|
+
@app.puts method( "#{ action }_action" ).call( *args ).to_yaml
|
33
|
+
return true
|
34
|
+
rescue StandardError => e
|
35
|
+
@app.puts e.to_yaml
|
36
|
+
end
|
37
|
+
return true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def upgen_action
|
44
|
+
@weblog.regenerate( :update )
|
45
|
+
"Regeneration complete"
|
46
|
+
end
|
47
|
+
|
48
|
+
def regen_action
|
49
|
+
@weblog.regenerate
|
50
|
+
"Regeneration complete"
|
51
|
+
end
|
52
|
+
|
53
|
+
def new_action
|
54
|
+
@weblog.entry_class.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def list_action( *inpath )
|
58
|
+
inpath = inpath.join '/'
|
59
|
+
@weblog.storage.find( :all => true, :inpath => inpath )
|
60
|
+
end
|
61
|
+
|
62
|
+
def search_action( words, *inpath )
|
63
|
+
inpath = inpath.join '/'
|
64
|
+
@weblog.storage.find( :all => true, :inpath => inpath, :search => words.split( ',' ) )
|
65
|
+
end
|
66
|
+
|
67
|
+
def post_action( *id )
|
68
|
+
id = id.join '/'
|
69
|
+
case @app.request_method
|
70
|
+
when "GET"
|
71
|
+
@weblog.storage.load_entry id
|
72
|
+
when "POST"
|
73
|
+
entry = YAML::load( @app.request_body )
|
74
|
+
@weblog.storage.save_entry id, entry
|
75
|
+
"Entry successfully saved."
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def edit_action
|
80
|
+
case @app.request_method
|
81
|
+
when "GET"
|
82
|
+
@weblog
|
83
|
+
when "POST"
|
84
|
+
config = YAML::load( @app.request_body )
|
85
|
+
config.save @weblog.hobix_yaml
|
86
|
+
"Weblog configuration saved."
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#
|
2
|
+
# = hobix/entry.rb
|
3
|
+
#
|
4
|
+
# Hobix command-line weblog system.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2003-2004 why the lucky stiff
|
7
|
+
#
|
8
|
+
# Written & maintained by why the lucky stiff <why@ruby-lang.org>
|
9
|
+
#
|
10
|
+
# This program is free software, released under a BSD license.
|
11
|
+
# See COPYING for details.
|
12
|
+
#
|
13
|
+
#--
|
14
|
+
# $Id$
|
15
|
+
#++
|
16
|
+
require 'redcloth'
|
17
|
+
require 'yaml'
|
18
|
+
|
19
|
+
module Hobix
|
20
|
+
class Article
|
21
|
+
end
|
22
|
+
end
|
data/lib/hobix/base.rb
ADDED
@@ -0,0 +1,477 @@
|
|
1
|
+
#
|
2
|
+
# = hobix/weblog.rb
|
3
|
+
#
|
4
|
+
# Hobix command-line weblog system.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2003-2004 why the lucky stiff
|
7
|
+
#
|
8
|
+
# Written & maintained by why the lucky stiff <why@ruby-lang.org>
|
9
|
+
#
|
10
|
+
# This program is free software, released under a BSD license.
|
11
|
+
# See COPYING for details.
|
12
|
+
#
|
13
|
+
#--
|
14
|
+
# $Id$
|
15
|
+
#++
|
16
|
+
require 'redcloth'
|
17
|
+
require 'yaml'
|
18
|
+
|
19
|
+
module YAML
|
20
|
+
class Omap
|
21
|
+
def keys; map { |k, v| k }; end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module Hobix
|
26
|
+
# The BasePlugin class is *bingo* the underlying class for
|
27
|
+
# all Hobix plugins. The +Class::inherited+ hook is used
|
28
|
+
# by this class to keep track of all classes that inherit
|
29
|
+
# from it.
|
30
|
+
class BasePlugin
|
31
|
+
@@plugins = {}
|
32
|
+
@@required_from = nil
|
33
|
+
# Initializes all the plugins, returning
|
34
|
+
# an Array of plugin objects. (Used by the
|
35
|
+
# +Hobix::Weblog+ class.)
|
36
|
+
def BasePlugin.start( req, opts, weblog )
|
37
|
+
@@required_from = req = req.dup
|
38
|
+
if req.tainted?
|
39
|
+
req.untaint if req =~ /^[\w\/\\]+$/
|
40
|
+
end
|
41
|
+
require( req )
|
42
|
+
@@required_from = nil
|
43
|
+
|
44
|
+
if @@plugins[req]
|
45
|
+
@@plugins[req].collect do |p|
|
46
|
+
if opts
|
47
|
+
p.new( weblog, opts )
|
48
|
+
else
|
49
|
+
p.new( weblog )
|
50
|
+
end
|
51
|
+
end
|
52
|
+
else
|
53
|
+
[]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
def BasePlugin.inherited( sub )
|
57
|
+
@@plugins[@@required_from] ||= []
|
58
|
+
@@plugins[@@required_from] << sub
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# The BaseStorage class outlines the fundamental API for
|
63
|
+
# all storage plugins. Storage plugins are responsible
|
64
|
+
# for abstracting away entry queries and managing the loading
|
65
|
+
# of Entry objects. The goal being: cache as much as you can,
|
66
|
+
# be efficient and tidy.
|
67
|
+
#
|
68
|
+
# == Query Methods
|
69
|
+
#
|
70
|
+
# find:: Each of the query methods below uses the +find+ method
|
71
|
+
# to perform its search. This method accepts a Hash of
|
72
|
+
# parameters. Please note that calling +find+ without
|
73
|
+
# parameters will return all entries which qualify for
|
74
|
+
# placement on the front page.
|
75
|
+
#
|
76
|
+
# all:: Returns all entries. Searches find( :all => true )
|
77
|
+
# lastn:: Returns the last _n_ entries which qualify for the
|
78
|
+
# front page.
|
79
|
+
# inpath:: Returns entries within a path which qualify for the
|
80
|
+
# front page.
|
81
|
+
# after:: Returns entries created after a given date.
|
82
|
+
# before:: Returns entries created before a given date.
|
83
|
+
# within:: Returns entries created between a start and
|
84
|
+
# end date.
|
85
|
+
#
|
86
|
+
class BaseStorage < BasePlugin
|
87
|
+
def initialize( weblog )
|
88
|
+
@link = weblog.link
|
89
|
+
end
|
90
|
+
def default_entry_id; "hobix-default-entry"; end
|
91
|
+
def default_entry( author )
|
92
|
+
Hobix::Entry.new do |e|
|
93
|
+
e.id = default_entry_id
|
94
|
+
e.link = e.class.url_link e, @link, "html"
|
95
|
+
e.created = Time.now
|
96
|
+
e.modified = Time.now
|
97
|
+
e.title = "This Ghostly Message From the Slime Will Soon Vanish!"
|
98
|
+
e.tagline = "A temporary message, a tingling sensation, Hobix is up!!"
|
99
|
+
e.author = author
|
100
|
+
e.content = Hobix::Entry.text_processor.new( "Welcome to Hobix! Once you make your first blog post, this entry will disappear. However, in the meantime, you can tweak the CSS of your blog until it suits your satisfaction and you have this bit of words to act as a place holder." )
|
101
|
+
end
|
102
|
+
end
|
103
|
+
def all
|
104
|
+
find( :all => true )
|
105
|
+
end
|
106
|
+
def lastn( n )
|
107
|
+
find( :lastn => ( n || 10 ) )
|
108
|
+
end
|
109
|
+
def inpath( path, n = nil )
|
110
|
+
find( :inpath => path, :lastn => n )
|
111
|
+
end
|
112
|
+
def after( after, n = nil )
|
113
|
+
find( :after => after, :lastn => n )
|
114
|
+
end
|
115
|
+
def before( before, n = nil )
|
116
|
+
find( :before => before, :lastn => n )
|
117
|
+
end
|
118
|
+
def match( expr )
|
119
|
+
find( :match => expr )
|
120
|
+
end
|
121
|
+
def within( after, before )
|
122
|
+
find( :after => after, :before => before )
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# The BaseOutput plugin is the underlying class for all output
|
127
|
+
# plugins. These plugins are associated to templates. Based on
|
128
|
+
# a template's suffix, the proper output plugin is loaded and
|
129
|
+
# used to generate page output.
|
130
|
+
class BaseOutput < BasePlugin
|
131
|
+
end
|
132
|
+
|
133
|
+
# The BasePublish plugin is the underlying class for all publishing
|
134
|
+
# plugins, which are notified of updates to pages.
|
135
|
+
#
|
136
|
+
# Publish plugins are executed after generation of the site. The plugin
|
137
|
+
# may choose to watch updates to certain types of pages. The plugin also
|
138
|
+
# receives a list of all the pages which have been updated.
|
139
|
+
#
|
140
|
+
# Generally, publish plugins fall into two categories:
|
141
|
+
#
|
142
|
+
# * Plugins which contact a service when certain updates happen.
|
143
|
+
# (Hobix includes an XML-RPC ping, which is triggered whenever
|
144
|
+
# the front page is updated.)
|
145
|
+
# * Plugins which transform Hobix output. (Hobix includes a
|
146
|
+
# replication plugin, which copies updated pages to a remote
|
147
|
+
# system via FTP or SFTP.)
|
148
|
+
#
|
149
|
+
# == Publish methods
|
150
|
+
#
|
151
|
+
# initialize( weblog, settings ):: Like all other plugins, the initialize method takes two parameters,
|
152
|
+
# a Hobix::Weblog object for the weblog being published and the
|
153
|
+
# settings data from the plugin's entry in hobix.yaml.
|
154
|
+
# watch:: (Optional) Returns an array of page types which, when published, activate the plugin.
|
155
|
+
# publish( pages ):: If pages are published and the watch criteria qualifies this plugin,
|
156
|
+
# this method is called with a hash of pages published. The key is the page type
|
157
|
+
# and the value is an array of Page objects.
|
158
|
+
class BasePublish < BasePlugin
|
159
|
+
end
|
160
|
+
|
161
|
+
# The BaseFacet plugin is the superclass for all plugins which have
|
162
|
+
# an interface (CGI, UI, etc.) These interfaces expose some functionality
|
163
|
+
# to the user through an entry form or series of views.
|
164
|
+
class BaseFacet < BasePlugin
|
165
|
+
def self.not_found app
|
166
|
+
app.send_not_found "Action `#{ app.action_uri }' not found. If this address should work, check your plugins."
|
167
|
+
end
|
168
|
+
def protect app, weblog
|
169
|
+
auth = ENV['HTTP_AUTHORIZATION'] || ENV['X-HTTP_AUTHORIZATION']
|
170
|
+
if auth
|
171
|
+
realm = 'Hobix login'
|
172
|
+
auth_type, auth = auth.split ' ', 2
|
173
|
+
authorized = false
|
174
|
+
case auth_type.downcase
|
175
|
+
when 'basic'
|
176
|
+
require 'base64'
|
177
|
+
name, pass = Base64::decode64( auth.strip ).split ':', 2
|
178
|
+
authorized = weblog.authorize name, pass
|
179
|
+
when 'digest'
|
180
|
+
require 'md5'
|
181
|
+
opts = {}
|
182
|
+
auth.gsub( /(\w+)="(.*?)"/ ) { opts[$1] = $2 }
|
183
|
+
app.puts opts.inspect
|
184
|
+
end
|
185
|
+
return true if authorized
|
186
|
+
end
|
187
|
+
|
188
|
+
app.send_unauthorized
|
189
|
+
# nonce = ["#{ Time.now.to_f }:#{ app.action_uri }"].pack("m").gsub /\s/, ''
|
190
|
+
# app.set_header 'WWW-Authenticate', %{Digest qop="auth", realm="#{ realm }", nonce="#{ nonce }", algorithm="MD5"}
|
191
|
+
app.set_header 'WWW-Authenticate', %{Basic realm="#{ realm }"}
|
192
|
+
false
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Enumerable::each_with_neighbors from Joel Vanderwerf's
|
197
|
+
# enum extenstions.
|
198
|
+
module Enumerable
|
199
|
+
def each_with_neighbors n = 1, empty = nil
|
200
|
+
nbrs = [empty] * (2 * n + 1)
|
201
|
+
offset = n
|
202
|
+
|
203
|
+
each { |x|
|
204
|
+
nbrs.shift
|
205
|
+
nbrs.push x
|
206
|
+
if offset == 0 # offset is now the offset of the first element, x0,
|
207
|
+
yield nbrs # of the sequence from the center of nbrs, or 0,
|
208
|
+
else # if x0 has already passed the center.
|
209
|
+
offset -= 1
|
210
|
+
end
|
211
|
+
}
|
212
|
+
|
213
|
+
n.times {
|
214
|
+
nbrs.shift
|
215
|
+
nbrs.push empty
|
216
|
+
if offset == 0
|
217
|
+
yield nbrs
|
218
|
+
else
|
219
|
+
offset -= 1
|
220
|
+
end
|
221
|
+
}
|
222
|
+
|
223
|
+
self
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
module BaseProperties
|
228
|
+
# Returns the complete list of properties for the immediate class.
|
229
|
+
# If called on an inheriting class, inherited properties are included.
|
230
|
+
module ClassMethods
|
231
|
+
def properties
|
232
|
+
if superclass.respond_to? :properties
|
233
|
+
s = superclass.properties.dup
|
234
|
+
(@__props || {}).each { |k, v| s[k] = v }
|
235
|
+
s
|
236
|
+
else
|
237
|
+
(@__props || {})
|
238
|
+
end
|
239
|
+
end
|
240
|
+
def prop_sections
|
241
|
+
if superclass.respond_to? :prop_sections
|
242
|
+
s = superclass.prop_sections.dup
|
243
|
+
(@__sects || {}).each { |k, v| s[k] = v }
|
244
|
+
s
|
245
|
+
else
|
246
|
+
(@__sects || {})
|
247
|
+
end
|
248
|
+
end
|
249
|
+
# Quick property definitions in class definitions.
|
250
|
+
def _ name, opts = nil
|
251
|
+
@__props ||= YAML::Omap[]
|
252
|
+
@__props[name] = opts
|
253
|
+
attr_accessor name unless method_defined? "#{ name }="
|
254
|
+
end
|
255
|
+
# Property sections
|
256
|
+
def _! name, opts = {}
|
257
|
+
@__sects ||= YAML::Omap[]
|
258
|
+
opts[:__sect] = @__props.last[0] rescue nil
|
259
|
+
@__sects[name] = opts
|
260
|
+
end
|
261
|
+
end
|
262
|
+
# Build a simple map of properties
|
263
|
+
def property_map
|
264
|
+
self.class.properties.map do |name, opts|
|
265
|
+
if opts
|
266
|
+
yreq = opts[:req] ? :req : :opt
|
267
|
+
["@#{ name }", yreq] if yreq
|
268
|
+
end
|
269
|
+
end.compact
|
270
|
+
end
|
271
|
+
# Build a property map for the YAML module
|
272
|
+
def to_yaml_properties
|
273
|
+
property_map.find_all do |prop, req|
|
274
|
+
case req
|
275
|
+
when :opt
|
276
|
+
not instance_variable_get( prop ).nil?
|
277
|
+
when :req
|
278
|
+
true
|
279
|
+
end
|
280
|
+
end.
|
281
|
+
collect do |prop, req|
|
282
|
+
prop
|
283
|
+
end
|
284
|
+
end
|
285
|
+
def self.append_features klass
|
286
|
+
super
|
287
|
+
klass.extend ClassMethods
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# placed here to avoid dependency cycle between base.rb and weblog.rb
|
292
|
+
class Weblog
|
293
|
+
@@entry_classes = []
|
294
|
+
def self.add_entry_class( c )
|
295
|
+
@@entry_classes << c
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
class BaseContent
|
300
|
+
include BaseProperties
|
301
|
+
|
302
|
+
_! 'Entry Information'
|
303
|
+
_ :id
|
304
|
+
_ :link
|
305
|
+
_ :title, :edit_as => :text, :search => :fulltext
|
306
|
+
_ :created, :edit_as => :datetime, :search => :prefix
|
307
|
+
_ :modified
|
308
|
+
_ :tags, :edit_as => :text, :search => :prefix
|
309
|
+
|
310
|
+
def initialize; yield self if block_given?; end
|
311
|
+
def day_id; created.strftime( "%Y/%m/%d" ) if created; end
|
312
|
+
def month_id; created.strftime( "%Y/%m" ) if created; end
|
313
|
+
def year_id; created.strftime( "%Y" ) if created; end
|
314
|
+
def section_id; File.dirname( id ) if id; end
|
315
|
+
def base_id; File.basename( id ) if id; end
|
316
|
+
def self.url_link( e, url = nil, ext = nil ); "#{ url }/#{ link_format e }#{ '.' + ext if ext }"; end
|
317
|
+
def self.link_format( e ); e.id; end
|
318
|
+
def force_tags; []; end
|
319
|
+
|
320
|
+
#
|
321
|
+
# If set to true, tags won't be deduced from the entry id
|
322
|
+
#
|
323
|
+
@@no_implicit_tags = false
|
324
|
+
|
325
|
+
def self.no_implicit_tags
|
326
|
+
@@no_implicit_tags = true
|
327
|
+
end
|
328
|
+
|
329
|
+
#
|
330
|
+
# When using implicit tag, the blog root (i.e) is not considered
|
331
|
+
# unless you set the value of +@@root_tag+ to what you need.
|
332
|
+
#
|
333
|
+
@@root_tag = nil
|
334
|
+
def self.root_tag=( tag )
|
335
|
+
@@root_tag = tag
|
336
|
+
end
|
337
|
+
|
338
|
+
#
|
339
|
+
# When computing so-called implicit 'implicit-tag', whether
|
340
|
+
# or not we should split the path into several tags
|
341
|
+
# (default: false)
|
342
|
+
#
|
343
|
+
@@split_implicit_tags = false
|
344
|
+
|
345
|
+
def self.split_implicit_tags
|
346
|
+
@@split_implicit_tags = true
|
347
|
+
end
|
348
|
+
|
349
|
+
#
|
350
|
+
# return an array of tags deduced from the path
|
351
|
+
# i.e. a path like ruby/hobix/foo.yml will lead
|
352
|
+
# to [ ruby, hobix ] tags
|
353
|
+
# Occurence of . (alone) will be either removed or replaced
|
354
|
+
# by the value of +root_tag+
|
355
|
+
#
|
356
|
+
def path_to_tags( path )
|
357
|
+
return [] if @@no_implicit_tags
|
358
|
+
return [] if path.nil?
|
359
|
+
if @@split_implicit_tags
|
360
|
+
tags_array = path.split("/").find_all { |e| e.size > 0 }
|
361
|
+
tags_array.pop # Last item is the entry title
|
362
|
+
else
|
363
|
+
tags_array = [ File.dirname( path )]
|
364
|
+
end
|
365
|
+
tags_array.map { |e| e == '.' ? @@root_tag : e }.compact
|
366
|
+
end
|
367
|
+
|
368
|
+
#
|
369
|
+
# return canonical tags, i.e. tags that are forced and that are deduced
|
370
|
+
# from the entry path
|
371
|
+
#
|
372
|
+
def canonical_tags( path=nil )
|
373
|
+
( force_tags + path_to_tags( path || self.id ) ).uniq
|
374
|
+
end
|
375
|
+
|
376
|
+
def tags;( canonical_tags + Array( @tags ) ).uniq; end
|
377
|
+
|
378
|
+
def self.yaml_type( tag )
|
379
|
+
# if self.respond_to? :yaml_as
|
380
|
+
# yaml_as tag
|
381
|
+
# else
|
382
|
+
if tag =~ /^tag:([^:]+):(.+)$/
|
383
|
+
define_method( :to_yaml_type ) { "!#$1/#$2" }
|
384
|
+
YAML::add_domain_type( $1, $2 ) { |t, v| self.maker( v ) }
|
385
|
+
end
|
386
|
+
# end
|
387
|
+
end
|
388
|
+
|
389
|
+
alias to_yaml_orig to_yaml
|
390
|
+
def to_yaml( opts = {} )
|
391
|
+
opts[:UseFold] = true if opts.respond_to? :[]
|
392
|
+
self.class.text_processor_fields.each do |f|
|
393
|
+
v = instance_variable_get( '@' + f )
|
394
|
+
if v.is_a? self.class.text_processor
|
395
|
+
instance_eval %{
|
396
|
+
def @#{ f }.to_yaml( opts = {} )
|
397
|
+
s = self.to_str
|
398
|
+
def s.to_yaml_style; :fold; end
|
399
|
+
s.to_yaml( opts )
|
400
|
+
end
|
401
|
+
}
|
402
|
+
end
|
403
|
+
end
|
404
|
+
to_yaml_orig( opts )
|
405
|
+
end
|
406
|
+
|
407
|
+
# Load the weblog entry from a file.
|
408
|
+
def self.load( file )
|
409
|
+
File.open( file ) { |f| YAML::load( f ) }
|
410
|
+
end
|
411
|
+
|
412
|
+
# Accessor which returns the text processor used for untyped
|
413
|
+
# strings in Entry fields. (defaults to +RedCloth+.)
|
414
|
+
def self.text_processor; RedCloth; end
|
415
|
+
# Returns an Array of fields to which the text processor applies.
|
416
|
+
def self.text_processor_fields
|
417
|
+
self.properties.map do |name, opts|
|
418
|
+
name.to_s if opts and opts[:text_processor]
|
419
|
+
end.compact
|
420
|
+
end
|
421
|
+
# Factory method for generating Entry classes from a hash. Used
|
422
|
+
# by the YAML loader.
|
423
|
+
def self.maker( val )
|
424
|
+
self::text_processor_fields.each do |f|
|
425
|
+
if val[f].respond_to? :value
|
426
|
+
str = val[f].value
|
427
|
+
def str.to_html
|
428
|
+
self
|
429
|
+
end
|
430
|
+
val[f] = str
|
431
|
+
elsif val[f].respond_to? :to_str
|
432
|
+
val[f] = self::text_processor.new( val[f].to_str )
|
433
|
+
end
|
434
|
+
end
|
435
|
+
YAML::object_maker( self, val )
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
# The BaseEntry class is the underlying class for all Hobix
|
440
|
+
# entries (i.e. the content for your website/blahhg.)
|
441
|
+
class BaseEntry < BaseContent
|
442
|
+
|
443
|
+
_ :id
|
444
|
+
_ :link
|
445
|
+
_ :title, :edit_as => :text, :search => :fulltext
|
446
|
+
_ :author, :req => true, :edit_as => :text, :search => :prefix
|
447
|
+
_ :contributors, :edit_as => :array, :search => :prefix
|
448
|
+
_ :created, :edit_as => :datetime, :search => :prefix
|
449
|
+
_ :modified
|
450
|
+
_ :tags, :edit_as => :text, :search => :prefix
|
451
|
+
_ :content, :edit_as => :textarea, :search => :fulltext, :text_processor => true
|
452
|
+
_ :content_ratings, :edit_as => :array
|
453
|
+
|
454
|
+
def content_ratings; @content_ratings || [:ham]; end
|
455
|
+
|
456
|
+
def self.inherited( sub )
|
457
|
+
Weblog::add_entry_class( sub )
|
458
|
+
end
|
459
|
+
|
460
|
+
# Build the searchable text
|
461
|
+
def to_search
|
462
|
+
self.class.properties.map do |name, opts|
|
463
|
+
next unless opts
|
464
|
+
val = instance_variable_get( "@#{ name }" )
|
465
|
+
next unless val
|
466
|
+
val = val.strftime "%Y-%m-%dT%H:%M:%S" if val.respond_to? :strftime
|
467
|
+
case opts[:search]
|
468
|
+
when :prefix
|
469
|
+
"#{ name }:" + val.to_s
|
470
|
+
when :fulltext
|
471
|
+
val.to_s
|
472
|
+
end
|
473
|
+
end.compact.join "\n"
|
474
|
+
end
|
475
|
+
|
476
|
+
end
|
477
|
+
end
|