hobix 0.4
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.
- 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
|