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
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'hobix/search/dictionary'
|
2
|
+
require 'hobix/search/vector'
|
3
|
+
|
4
|
+
module Hobix
|
5
|
+
module Search
|
6
|
+
module Simple
|
7
|
+
class Contents < Array
|
8
|
+
def latest_mtime
|
9
|
+
latest_mtime = Time.at(0)
|
10
|
+
each do |item|
|
11
|
+
if(item.mtime > latest_mtime)
|
12
|
+
latest_mtime = item.mtime
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Content
|
19
|
+
attr_accessor :content, :identifier, :mtime, :classifications
|
20
|
+
def initialize(content, identifier, mtime, clsf)
|
21
|
+
@content = content
|
22
|
+
@identifier = identifier
|
23
|
+
@mtime = mtime
|
24
|
+
@classifications = clsf
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
SearchResult = Struct.new(:name, :score)
|
29
|
+
|
30
|
+
class SearchResult
|
31
|
+
# enable sort by score
|
32
|
+
def <=>(other)
|
33
|
+
self.score <=> other.score
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class SearchResults
|
38
|
+
attr_reader :warnings
|
39
|
+
attr_reader :results
|
40
|
+
|
41
|
+
|
42
|
+
def initialize
|
43
|
+
@warnings = []
|
44
|
+
@results = {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_warning(txt)
|
48
|
+
@warnings << txt
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_result(name, score)
|
52
|
+
@results[name] = SearchResult.new(name, score)
|
53
|
+
end
|
54
|
+
|
55
|
+
def contains_matches
|
56
|
+
!@results.empty?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
class Searcher
|
62
|
+
|
63
|
+
def initialize(dict, document_vectors, cache_file)
|
64
|
+
@dict = dict
|
65
|
+
@document_vectors = document_vectors
|
66
|
+
@cache_file = cache_file
|
67
|
+
end
|
68
|
+
|
69
|
+
# Return SearchResults based on trying to find the array of
|
70
|
+
# +words+ in our document vectors
|
71
|
+
#
|
72
|
+
# A word beginning '+' _must_ appear in the target documents
|
73
|
+
# A word beginning '-' <i>must not</i> appear
|
74
|
+
# other words are scored. The documents with the highest
|
75
|
+
# scores are returned first
|
76
|
+
|
77
|
+
def find_words(words)
|
78
|
+
search_results = SearchResults.new
|
79
|
+
|
80
|
+
general = Vector.new
|
81
|
+
must_match = Vector.new
|
82
|
+
must_not_match = Vector.new
|
83
|
+
not_found = false
|
84
|
+
|
85
|
+
extract_words_for_searcher(words.join(' ')) do |word|
|
86
|
+
case word[0]
|
87
|
+
when ?+
|
88
|
+
word = word[1,99]
|
89
|
+
vector = must_match
|
90
|
+
when ?-
|
91
|
+
word = word[1,99]
|
92
|
+
vector = must_not_match
|
93
|
+
else
|
94
|
+
vector = general
|
95
|
+
end
|
96
|
+
|
97
|
+
index = @dict.find(word.downcase)
|
98
|
+
if index
|
99
|
+
vector.add_word_index(index)
|
100
|
+
else
|
101
|
+
not_found = true
|
102
|
+
search_results.add_warning "'#{word}' does not occur in the documents"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
if (general.num_bits + must_match.num_bits).zero?
|
107
|
+
search_results.add_warning "No valid search terms given"
|
108
|
+
elsif not not_found
|
109
|
+
res = []
|
110
|
+
@document_vectors.each do |entry, (dvec, mtime)|
|
111
|
+
score = dvec.score_against(must_match, must_not_match, general)
|
112
|
+
res << [ entry, score ] if score > 0
|
113
|
+
end
|
114
|
+
|
115
|
+
res.sort {|a,b| b[1] <=> a[1] }.each {|name, score|
|
116
|
+
search_results.add_result(name, score)
|
117
|
+
}
|
118
|
+
|
119
|
+
search_results.add_warning "No matches" unless search_results.contains_matches
|
120
|
+
end
|
121
|
+
search_results
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# Serialization support. At some point we'll need to do incremental indexing.
|
126
|
+
# For now, however, the following seems to work fairly effectively
|
127
|
+
# on 1000 entry blogs, so I'll defer the change until later.
|
128
|
+
def Searcher.load(cache_file, wash=false)
|
129
|
+
dict = document_vectors = nil
|
130
|
+
modified = false
|
131
|
+
loaded = false
|
132
|
+
begin
|
133
|
+
File.open(cache_file, "r") do |f|
|
134
|
+
unless wash
|
135
|
+
dict = Marshal.load(f)
|
136
|
+
document_vectors = Marshal.load(f)
|
137
|
+
loaded = true
|
138
|
+
end
|
139
|
+
end
|
140
|
+
rescue
|
141
|
+
;
|
142
|
+
end
|
143
|
+
|
144
|
+
unless loaded
|
145
|
+
dict = Dictionary.new
|
146
|
+
document_vectors = {}
|
147
|
+
modified = true
|
148
|
+
end
|
149
|
+
|
150
|
+
s = Searcher.new(dict, document_vectors, cache_file)
|
151
|
+
s.dump if modified
|
152
|
+
s
|
153
|
+
end
|
154
|
+
|
155
|
+
def dump
|
156
|
+
File.open(@cache_file, "w") do |fileInstance|
|
157
|
+
Marshal.dump(@dict, fileInstance)
|
158
|
+
Marshal.dump(@document_vectors, fileInstance)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def extract_words_for_searcher(text)
|
163
|
+
text.scan(/[-+]?\w[\-\w:\\]{2,}/) do |word|
|
164
|
+
yield word
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def has_entry? id, mtime
|
169
|
+
dvec = @document_vectors[id]
|
170
|
+
return true if dvec and dvec.at.to_i >= mtime.to_i
|
171
|
+
end
|
172
|
+
|
173
|
+
# Create a new dictionary and document vectors from
|
174
|
+
# a blog archive
|
175
|
+
|
176
|
+
def catalog(entry)
|
177
|
+
unless has_entry? entry.identifier, entry.mtime
|
178
|
+
vector = Vector.new
|
179
|
+
vector.at = entry.mtime
|
180
|
+
extract_words_for_searcher(entry.content.downcase) do |word|
|
181
|
+
word_index = @dict.add_word(word, entry.classifications)
|
182
|
+
if word_index
|
183
|
+
vector.add_word_index(word_index)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
@document_vectors[entry.identifier] = vector
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def classifications(text)
|
191
|
+
score = Hash.new
|
192
|
+
@dict.clsf.each do |category, category_words|
|
193
|
+
score[category] = 0
|
194
|
+
total = category_words.values.inject(0) {|sum, element| sum+element}
|
195
|
+
extract_words_for_searcher(text) do |word|
|
196
|
+
s = category_words.has_key?(word) ? category_words[word] : 0.1
|
197
|
+
score[category] += Math.log(s/total.to_f)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
score
|
201
|
+
end
|
202
|
+
|
203
|
+
def classify(text)
|
204
|
+
(classifications(text).sort_by { |a| -a[1] })[0][0]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# Maintain a vector of words, where a word is represented by
|
2
|
+
# its index in our Dictionary
|
3
|
+
#
|
4
|
+
module Hobix
|
5
|
+
module Search
|
6
|
+
module Simple
|
7
|
+
class Vector
|
8
|
+
|
9
|
+
attr_accessor :at
|
10
|
+
attr_reader :num_bits, :max_bit, :bits
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
# @bits = []
|
14
|
+
@bits = 0
|
15
|
+
@max_bit = -1
|
16
|
+
@num_bits = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_word_index(index)
|
20
|
+
if @bits[index].zero?
|
21
|
+
@bits += (1 << index)
|
22
|
+
@num_bits += 1
|
23
|
+
@max_bit = index if @max_bit < index
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def dot(vector)
|
28
|
+
# We only need to calculate up to the end of the shortest vector
|
29
|
+
limit = @max_bit
|
30
|
+
# Commenting out the next line makes this vector the dominant
|
31
|
+
# one when doing the comparison
|
32
|
+
limit = vector.max_bit if limit > vector.max_bit
|
33
|
+
|
34
|
+
# because both vectors have just ones or zeros in them,
|
35
|
+
# we can pre-calculate the AnBn component
|
36
|
+
# The vector's magnitude is Sqrt(num set bits)
|
37
|
+
factor = Math.sqrt(1.0/@num_bits) * Math.sqrt(1.0/vector.num_bits)
|
38
|
+
|
39
|
+
count = 0
|
40
|
+
(limit+1).times {|i| count += 1 if @bits[i] ==1 && vector.bits[i] == 1}
|
41
|
+
|
42
|
+
factor * count
|
43
|
+
end
|
44
|
+
|
45
|
+
# We're a document's vector, and we're being matched against
|
46
|
+
# three other vectors:
|
47
|
+
# 1. A list of <i>must match</i> words
|
48
|
+
# 2. A list of <i>must not match</i> words
|
49
|
+
# 3. A list of general words. The score we return
|
50
|
+
# is the number of these that we match
|
51
|
+
|
52
|
+
def score_against(must_match, must_not_match, general)
|
53
|
+
# Eliminate if any _must_not_match_ words found
|
54
|
+
unless must_not_match.num_bits.zero?
|
55
|
+
return 0 unless (@bits & must_not_match.bits).zero?
|
56
|
+
end
|
57
|
+
|
58
|
+
# If the match was entirely negative, then we know we're passed at
|
59
|
+
# this point
|
60
|
+
|
61
|
+
if must_match.num_bits.zero? and general.num_bits.zero?
|
62
|
+
return 1
|
63
|
+
end
|
64
|
+
|
65
|
+
count = 0
|
66
|
+
|
67
|
+
# Eliminate unless all _must_match_ words found
|
68
|
+
|
69
|
+
unless must_match.num_bits.zero?
|
70
|
+
return 0 unless (@bits & must_match.bits) == must_match.bits
|
71
|
+
count = 1
|
72
|
+
end
|
73
|
+
|
74
|
+
# finally score on the rest
|
75
|
+
common = general.bits & @bits
|
76
|
+
count += count_bits(common, @max_bit+1) unless common.zero?
|
77
|
+
count
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def count_bits(word, max_bit)
|
83
|
+
res = 0
|
84
|
+
((max_bit+29)/30).times do |offset|
|
85
|
+
x = (word >> (offset*30)) & 0x3fffffff
|
86
|
+
next if x.zero?
|
87
|
+
x = x - ((x >> 1) & 0x55555555)
|
88
|
+
x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
|
89
|
+
x = (x + (x >> 4)) & 0x0f0f0f0f;
|
90
|
+
x = x + (x >> 8)
|
91
|
+
x = x + (x >> 16)
|
92
|
+
res += x & 0x3f
|
93
|
+
end
|
94
|
+
res
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,398 @@
|
|
1
|
+
#
|
2
|
+
# = hobix/storage/filesys.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 'find'
|
17
|
+
require 'yaml'
|
18
|
+
require 'fileutils'
|
19
|
+
# require 'hobix/search/simple'
|
20
|
+
|
21
|
+
module Hobix
|
22
|
+
|
23
|
+
#
|
24
|
+
# The IndexEntry class
|
25
|
+
#
|
26
|
+
class IndexEntry < BaseContent
|
27
|
+
def initialize( entry, fields = self.class.properties.keys )
|
28
|
+
fields.each do |field|
|
29
|
+
val = if entry.respond_to? field
|
30
|
+
entry.send( field )
|
31
|
+
elsif respond_to? "make_#{field}"
|
32
|
+
send( "make_#{field}", entry )
|
33
|
+
else
|
34
|
+
:unset
|
35
|
+
end
|
36
|
+
send( "#{field}=", val )
|
37
|
+
end
|
38
|
+
yield self if block_given?
|
39
|
+
end
|
40
|
+
|
41
|
+
yaml_type "!hobix.com,2004/storage/indexEntry"
|
42
|
+
end
|
43
|
+
|
44
|
+
module Storage
|
45
|
+
|
46
|
+
#
|
47
|
+
# The FileSys class is a storage plugin, it manages the loading and dumping of
|
48
|
+
# Hobix entries and attachments. The FileSys class also keeps an index of entry
|
49
|
+
# information, to keep the system from loading unneeded entries.
|
50
|
+
class FileSys < Hobix::BaseStorage
|
51
|
+
# Start the storage plugin for the +weblog+ passed in.
|
52
|
+
def initialize( weblog )
|
53
|
+
super( weblog )
|
54
|
+
@modified = {}
|
55
|
+
@basepath = weblog.entry_path
|
56
|
+
@default_author = weblog.authors.keys.first
|
57
|
+
@weblog = weblog
|
58
|
+
end
|
59
|
+
|
60
|
+
def now; Time.at( Time.now.to_i ); end
|
61
|
+
|
62
|
+
# The default extension for entries. Defaults to: yaml.
|
63
|
+
def extension
|
64
|
+
'yaml'
|
65
|
+
end
|
66
|
+
|
67
|
+
# Determine if +id+ is a valid entry identifier, untaint if so.
|
68
|
+
def check_id( id )
|
69
|
+
id.untaint if id.tainted? and id =~ /^[\w\/\\]+$/
|
70
|
+
end
|
71
|
+
|
72
|
+
# Build an entry's complete path based on its +id+. Optionally, extension +ext+ can
|
73
|
+
# be used to find the path of attachments.
|
74
|
+
def entry_path( id, ext = extension )
|
75
|
+
File.join( @basepath, id.split( '/' ) ) + "." + ext
|
76
|
+
end
|
77
|
+
|
78
|
+
# Brings an entry's modified time current.
|
79
|
+
def touch_entry( id )
|
80
|
+
check_id( id )
|
81
|
+
@modified[id] = Time.now
|
82
|
+
FileUtils.touch entry_path( id )
|
83
|
+
end
|
84
|
+
|
85
|
+
# Save the entry object +e+ and identify it as +id+. The +create_category+ flag
|
86
|
+
# will forcefully make the needed directories.
|
87
|
+
def save_entry( id, e, create_category=false )
|
88
|
+
load_index
|
89
|
+
check_id( id )
|
90
|
+
e.created ||= (@index.has_key?( id ) ? @index[id].created : now)
|
91
|
+
path = entry_path( id )
|
92
|
+
|
93
|
+
begin
|
94
|
+
File.open( path, 'w' ) { |f| YAML::dump( e, f ) }
|
95
|
+
rescue Errno::ENOENT
|
96
|
+
raise unless create_category and File.exists? @basepath
|
97
|
+
FileUtils.makedirs File.dirname( path )
|
98
|
+
retry
|
99
|
+
end
|
100
|
+
|
101
|
+
@entry_cache ||= {}
|
102
|
+
e.id = id
|
103
|
+
e.link = e.class.url_link e, @link, @weblog.central_ext
|
104
|
+
e.modified = now
|
105
|
+
@entry_cache[id] = e
|
106
|
+
|
107
|
+
@index[id] = @weblog.index_class.new( e ) do |i|
|
108
|
+
i.modified = e.modified
|
109
|
+
end
|
110
|
+
@modified[id] = e.modified
|
111
|
+
# catalog_search_entry( e )
|
112
|
+
sort_index( true )
|
113
|
+
e
|
114
|
+
end
|
115
|
+
|
116
|
+
# Loads the entry object identified by +id+. Entries are cached for future loading.
|
117
|
+
def load_entry( id )
|
118
|
+
return default_entry( @default_author ) if id == default_entry_id
|
119
|
+
load_index
|
120
|
+
check_id( id )
|
121
|
+
@entry_cache ||= {}
|
122
|
+
unless @entry_cache.has_key? id
|
123
|
+
entry_file = entry_path( id )
|
124
|
+
e = Hobix::Entry::load( entry_file )
|
125
|
+
e.id = id
|
126
|
+
e.link = e.class.url_link e, @link, @weblog.central_ext
|
127
|
+
e.modified = modified( id )
|
128
|
+
unless e.created
|
129
|
+
e.created = @index[id].created
|
130
|
+
File.open( entry_file, 'w' ) { |f| YAML::dump( e, f ) }
|
131
|
+
end
|
132
|
+
@entry_cache[id] = e
|
133
|
+
else
|
134
|
+
@entry_cache[id]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Loads the search engine database. The database will be cleansed and re-scanned if +wash+ is true.
|
139
|
+
# def load_search_index( wash )
|
140
|
+
# @search_index = Hobix::Search::Simple::Searcher.load( File.join( @basepath, 'index.search' ), wash )
|
141
|
+
# end
|
142
|
+
|
143
|
+
# Catalogs an entry object +e+ in the search engine.
|
144
|
+
# def catalog_search_entry( e )
|
145
|
+
# @search_index.catalog( Hobix::Search::Simple::Content.new( e.to_search, e.id, e.modified, e.content_ratings ) )
|
146
|
+
# end
|
147
|
+
|
148
|
+
# Determines if the search engine has already scanned an entry represented by IndexEntry +ie+.
|
149
|
+
# def search_needs_update? ie
|
150
|
+
# not @search_index.has_entry? ie.id, ie.modified
|
151
|
+
# end
|
152
|
+
|
153
|
+
# Load the internal index (saved at @entry_path/index.hobix) and refresh any timestamps
|
154
|
+
# which may be stale.
|
155
|
+
def load_index
|
156
|
+
return false if @index
|
157
|
+
index_path = File.join( @basepath, 'index.hobix' )
|
158
|
+
index = if File.exists? index_path
|
159
|
+
YAML::load( File.open( index_path ) )
|
160
|
+
else
|
161
|
+
YAML::Omap::new
|
162
|
+
end
|
163
|
+
@index = YAML::Omap::new
|
164
|
+
# load_search_index( index.length == 0 )
|
165
|
+
|
166
|
+
modified = false
|
167
|
+
index_fields = @weblog.index_class.properties.keys
|
168
|
+
Find::find( @basepath ) do |path|
|
169
|
+
path.untaint
|
170
|
+
if FileTest.directory? path
|
171
|
+
Find.prune if File.basename(path)[0] == ?.
|
172
|
+
else
|
173
|
+
entry_path = path.gsub( /^#{ Regexp::quote( @basepath ) }\/?/, '' )
|
174
|
+
next if entry_path !~ /\.#{ Regexp::quote( extension ) }$/
|
175
|
+
entry_paths = File.split( $` )
|
176
|
+
entry_paths.shift if entry_paths.first == '.'
|
177
|
+
entry_id = entry_paths.join( '/' )
|
178
|
+
@modified[entry_id] = File.mtime( path )
|
179
|
+
|
180
|
+
index_entry = nil
|
181
|
+
if ( index.has_key? entry_id ) and !( index[entry_id].is_a? ::Time ) # pre-0.4 index format
|
182
|
+
index_entry = index[entry_id]
|
183
|
+
end
|
184
|
+
## we will (re)load the entry if:
|
185
|
+
if not index_entry.respond_to?( :modified ) or # it's new
|
186
|
+
( index_entry.modified != @modified[entry_id] ) or # it's changed
|
187
|
+
index_fields.detect { |f| index_entry.send( f ).nil? } # index fields have been added
|
188
|
+
# or search_needs_update? index_entry # entry is old or not available in search db
|
189
|
+
|
190
|
+
efile = entry_path( entry_id )
|
191
|
+
e = Hobix::Entry::load( efile )
|
192
|
+
e.id = entry_id
|
193
|
+
index_entry = @weblog.index_class.new( e, index_fields ) do |i|
|
194
|
+
i.modified = @modified[entry_id]
|
195
|
+
end
|
196
|
+
# catalog_search_entry( e )
|
197
|
+
modified = true
|
198
|
+
end
|
199
|
+
@index[index_entry.id] = index_entry
|
200
|
+
end
|
201
|
+
end
|
202
|
+
sort_index( modified )
|
203
|
+
true
|
204
|
+
end
|
205
|
+
|
206
|
+
# Sorts the internal entry index (used by load_index.)
|
207
|
+
def sort_index( modified )
|
208
|
+
return unless @index
|
209
|
+
index_path = File.join( @basepath, 'index.hobix' )
|
210
|
+
@index.sort! { |x,y| y[1].created <=> x[1].created }
|
211
|
+
if modified
|
212
|
+
File.open( index_path, 'w' ) do |f|
|
213
|
+
YAML::dump( @index, f )
|
214
|
+
end
|
215
|
+
# @search_index.dump
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Returns a Hobix::Storage::FileSys object with its scope limited
|
220
|
+
# to entries inside a certain path +p+.
|
221
|
+
def path_storage( p )
|
222
|
+
return self if ['', '.'].include? p
|
223
|
+
load_index
|
224
|
+
path_storage = self.dup
|
225
|
+
path_storage.instance_eval do
|
226
|
+
@index = @index.dup.delete_if do |id, entry|
|
227
|
+
if id.index( p ) != 0
|
228
|
+
@modified.delete( p )
|
229
|
+
true
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
path_storage
|
234
|
+
end
|
235
|
+
|
236
|
+
# Returns an Array all `sections', or directories which contain entries.
|
237
|
+
# If you have three entries: `news/article1', `about/me', and `news/misc/article2',
|
238
|
+
# then you have three sections: `news', `about', `news/misc'.
|
239
|
+
def sections( opts = nil )
|
240
|
+
load_index
|
241
|
+
hsh = {}
|
242
|
+
@index.collect { |id, e| e.section_id }.uniq.sort
|
243
|
+
end
|
244
|
+
|
245
|
+
# Find entries based on criteria from the +search+ hash.
|
246
|
+
# Possible criteria include:
|
247
|
+
#
|
248
|
+
# :after:: Select entries created after a given Time.
|
249
|
+
# :before:: Select entries created before a given Time.
|
250
|
+
# :inpath:: Select entries contained within a path.
|
251
|
+
# :match:: Select entries with an +id+ which match a Regexp.
|
252
|
+
# :search:: Fulltext search of entries for search words.
|
253
|
+
# :lastn:: Limit the search to include only a given number of entries.
|
254
|
+
#
|
255
|
+
# This method returns an Array of +IndexEntry+ objects for use in
|
256
|
+
# skel_* methods.
|
257
|
+
def find( search = {} )
|
258
|
+
load_index
|
259
|
+
_index = @index
|
260
|
+
if _index.empty?
|
261
|
+
e = default_entry( @default_author )
|
262
|
+
@modified[e.id] = e.modified
|
263
|
+
_index = {e.id => @weblog.index_class.new(e)}
|
264
|
+
end
|
265
|
+
# if search[:search]
|
266
|
+
# sr = @search_index.find_words( search[:search] )
|
267
|
+
# end
|
268
|
+
unless search[:all]
|
269
|
+
ignore_test = nil
|
270
|
+
ignored = @weblog.sections_ignored
|
271
|
+
unless ignored.empty?
|
272
|
+
ignore_test = /^(#{ ignored.collect { |i| Regexp.quote( i ) }.join( '|' ) })/
|
273
|
+
end
|
274
|
+
end
|
275
|
+
entries = _index.collect do |id, entry|
|
276
|
+
skip = false
|
277
|
+
if ignore_test and not search[:all]
|
278
|
+
skip = entry.id =~ ignore_test
|
279
|
+
end
|
280
|
+
search.each do |skey, sval|
|
281
|
+
break if skip
|
282
|
+
skip = case skey
|
283
|
+
when :after
|
284
|
+
entry.created < sval
|
285
|
+
when :before
|
286
|
+
entry.created > sval
|
287
|
+
when :inpath
|
288
|
+
entry.id.index( sval ) != 0
|
289
|
+
when :match
|
290
|
+
not entry.id.match sval
|
291
|
+
# when :search
|
292
|
+
# not sr.results[entry.id]
|
293
|
+
else
|
294
|
+
false
|
295
|
+
end
|
296
|
+
end
|
297
|
+
if skip then nil else entry end
|
298
|
+
end.compact
|
299
|
+
entries.slice!( search[:lastn]..-1 ) if search[:lastn] and entries.length > search[:lastn]
|
300
|
+
entries
|
301
|
+
end
|
302
|
+
|
303
|
+
# Returns a Time object for the latest modified time for a group of
|
304
|
+
# +entries+ (pass in an Array of IndexEntry objects).
|
305
|
+
def last_modified( entries )
|
306
|
+
entries.collect do |entry|
|
307
|
+
modified( entry.id )
|
308
|
+
end.max
|
309
|
+
end
|
310
|
+
|
311
|
+
# Returns a Time object for the latest creation time for a group of
|
312
|
+
# +entries+ (pass in an Array of IndexEntry objects).
|
313
|
+
def last_created( entries )
|
314
|
+
entries.collect do |entry|
|
315
|
+
entry.created
|
316
|
+
end.max
|
317
|
+
end
|
318
|
+
|
319
|
+
# Returns a Time object representing the +modified+ time for the
|
320
|
+
# entry identified by +entry_id+.
|
321
|
+
def modified( entry_id )
|
322
|
+
find_attached( entry_id ).inject( @modified[entry_id] ) do |max, ext|
|
323
|
+
mtime = File.mtime( entry_path( entry_id, ext ) )
|
324
|
+
mtime > max ? mtime : max
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# Returns an Array of Arrays representing the months which contain
|
329
|
+
# +entries+ (pass in an Array of IndexEntry objects).
|
330
|
+
#
|
331
|
+
# See Hobix::Weblog.skel_month for an example of this method's usage.
|
332
|
+
def get_months( entries )
|
333
|
+
return [] if entries.empty?
|
334
|
+
first_time = entries.collect { |e| e.created }.min
|
335
|
+
last_time = entries.collect { |e| e.created }.max
|
336
|
+
start = Time.mktime( first_time.year, first_time.month, 1 )
|
337
|
+
stop = Time.mktime( last_time.year, last_time.month, last_time.day )
|
338
|
+
months = []
|
339
|
+
until start > stop
|
340
|
+
next_year, next_month = start.year, start.month + 1
|
341
|
+
if next_month > 12
|
342
|
+
next_year += next_month / 12
|
343
|
+
next_month %= 12
|
344
|
+
end
|
345
|
+
month_end = Time.mktime( next_year, next_month, 1 ) - 1
|
346
|
+
months << [ start, month_end, start.strftime( "/%Y/%m/" ) ]
|
347
|
+
start = month_end + 1
|
348
|
+
end
|
349
|
+
months
|
350
|
+
end
|
351
|
+
|
352
|
+
# Discovers attachments to an entry identified by +id+.
|
353
|
+
def find_attached( id )
|
354
|
+
check_id( id )
|
355
|
+
Dir[ entry_path( id, '*' ) ].collect do |att|
|
356
|
+
atp = att.match( /#{ Regexp::quote( id ) }\.(?!#{ extension }$)/ )
|
357
|
+
atp.post_match if atp
|
358
|
+
end.compact
|
359
|
+
end
|
360
|
+
|
361
|
+
# Loads an attachment to an entry identified by +id+. Entries
|
362
|
+
# can have any kind of YAML attachment, each which a specific extension.
|
363
|
+
def load_attached( id, ext )
|
364
|
+
check_id( id )
|
365
|
+
@attach_cache ||= {}
|
366
|
+
file_id = "#{ id }.#{ ext }"
|
367
|
+
unless @attach_cache.has_key? file_id
|
368
|
+
@attach_cache[id] = File.open( entry_path( id, ext ) ) do |f|
|
369
|
+
YAML::load( f )
|
370
|
+
end
|
371
|
+
else
|
372
|
+
@attach_cache[id]
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# Saves an attachment to an entry identified by +id+. The attachment
|
377
|
+
# +e+ is saved with an extension +ext+.
|
378
|
+
def save_attached( id, ext, e )
|
379
|
+
check_id( id )
|
380
|
+
File.open( entry_path( id, ext ), 'w' ) do |f|
|
381
|
+
YAML::dump( e, f )
|
382
|
+
end
|
383
|
+
|
384
|
+
@attach_cache ||= {}
|
385
|
+
@attach_cache[id] = e
|
386
|
+
end
|
387
|
+
|
388
|
+
# Appends the given items to an entry attachment with the given type, and
|
389
|
+
# then saves the modified attachment. If an attachment of the given type
|
390
|
+
# does not exist, it will be created.
|
391
|
+
def append_to_attachment( entry_id, attachment_type, *items )
|
392
|
+
attachment = load_attached( entry_id, attachment_type ) rescue []
|
393
|
+
attachment += items
|
394
|
+
save_attached( entry_id, attachment_type, attachment )
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|