ferret 0.11.3 → 0.11.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +7 -1
- data/bin/ferret-browser +79 -0
- data/ext/analysis.c +5 -2
- data/ext/config.h +2 -1
- data/ext/ferret.c +32 -7
- data/ext/ferret.h +1 -0
- data/ext/index.c +69 -48
- data/ext/q_boolean.c +21 -7
- data/ext/q_parser.c +203 -113
- data/ext/q_span.c +2 -1
- data/ext/r_analysis.c +14 -1
- data/ext/r_index.c +179 -0
- data/ext/r_search.c +12 -30
- data/ext/search.c +1 -0
- data/ext/search.h +4 -0
- data/ext/store.c +24 -0
- data/ext/store.h +14 -0
- data/lib/ferret/browser.rb +246 -0
- data/lib/ferret/browser/s/global.js +192 -0
- data/lib/ferret/browser/s/style.css +148 -0
- data/lib/ferret/browser/views/document/list.rhtml +49 -0
- data/lib/ferret/browser/views/document/show.rhtml +27 -0
- data/lib/ferret/browser/views/error/index.rhtml +7 -0
- data/lib/ferret/browser/views/help/index.rhtml +8 -0
- data/lib/ferret/browser/views/home/index.rhtml +29 -0
- data/lib/ferret/browser/views/layout.rhtml +22 -0
- data/lib/ferret/browser/views/term-vector/index.rhtml +4 -0
- data/lib/ferret/browser/views/term/index.rhtml +199 -0
- data/lib/ferret/browser/views/term/termdocs.rhtml +1 -0
- data/lib/ferret/browser/webrick.rb +14 -0
- data/lib/ferret/index.rb +67 -36
- data/lib/ferret_version.rb +1 -1
- data/test/unit/analysis/tc_analyzer.rb +5 -5
- data/test/unit/analysis/tc_token_stream.rb +4 -4
- data/test/unit/index/tc_index.rb +1 -1
- data/test/unit/index/tc_index_reader.rb +37 -0
- data/test/unit/search/tc_spans.rb +18 -1
- metadata +18 -5
data/ext/store.h
CHANGED
@@ -705,6 +705,20 @@ extern __inline off_t is_read_voff_t(InStream *is);
|
|
705
705
|
*/
|
706
706
|
extern char *is_read_string(InStream *is);
|
707
707
|
|
708
|
+
/**
|
709
|
+
* Read a string from the InStream. A string is an integer +length+ in vint
|
710
|
+
* format (see is_read_vint) followed by +length+ bytes. This is the format
|
711
|
+
* used by os_write_string. This method is similar to +is_read_string+ except
|
712
|
+
* that it will safely free all memory if there is an error reading the
|
713
|
+
* string.
|
714
|
+
*
|
715
|
+
* @param is the InStream to read from
|
716
|
+
* @return a null byte delimited string
|
717
|
+
* @raise IO_ERROR if there is a error reading from the file-system
|
718
|
+
* @raise EOF_ERROR if there is an attempt to read past the end of the file
|
719
|
+
*/
|
720
|
+
extern char *is_read_string_safe(InStream *is);
|
721
|
+
|
708
722
|
/**
|
709
723
|
* Copy cnt bytes from Instream _is_ to OutStream _os_.
|
710
724
|
*
|
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
|
4
|
+
module Ferret::Browser
|
5
|
+
class Delegator
|
6
|
+
def initialize(reader, path)
|
7
|
+
@reader, @path = reader, path
|
8
|
+
end
|
9
|
+
|
10
|
+
def run(env)
|
11
|
+
controller, action, args = :home, :index, nil
|
12
|
+
query_string = env['QUERY_STRING']||''
|
13
|
+
params = parse_query_string(query_string)
|
14
|
+
req_path = env['PATH_INFO'].gsub(/\/+/, '/')
|
15
|
+
case req_path
|
16
|
+
when '/'
|
17
|
+
# nothing to do
|
18
|
+
when /^\/?([-a-zA-Z]+)\/?$/
|
19
|
+
controller = $1
|
20
|
+
when /^\/?([-a-zA-Z]+)\/([-a-zA-Z]+)\/?(.*)?$/
|
21
|
+
controller = $1
|
22
|
+
action = $2
|
23
|
+
args = $3
|
24
|
+
else
|
25
|
+
controller = :error
|
26
|
+
args = req_path
|
27
|
+
end
|
28
|
+
controller_vars = {
|
29
|
+
:params => params,
|
30
|
+
:req_path => req_path,
|
31
|
+
:query_string => query_string,
|
32
|
+
}
|
33
|
+
delegate(controller, action, args, controller_vars)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def delegate(controller, action, args, controller_vars)
|
39
|
+
begin
|
40
|
+
controller = to_const(controller, 'Controller').
|
41
|
+
new(@reader, @path, controller_vars)
|
42
|
+
controller.send(action, args)
|
43
|
+
rescue Exception => e
|
44
|
+
puts e.to_s
|
45
|
+
controller_vars[:params][:error] = e
|
46
|
+
ErrorController.new(@reader, @path, controller_vars).index
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_const(str, suffix='')
|
51
|
+
Ferret::Browser.const_get(str.to_s.split('-').
|
52
|
+
map {|w| w.capitalize}.join('') + suffix)
|
53
|
+
end
|
54
|
+
|
55
|
+
# from _why's camping
|
56
|
+
def unescape_uri(s)
|
57
|
+
s.tr('+', ' ').gsub(/%([\da-f]{2})/in){[$1].pack('H*')}
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_query_string(query_string, delim = '&;')
|
61
|
+
m = proc {|_,o,n| o.update(n, &m) rescue ([*o] << n)}
|
62
|
+
(query_string||'').split(/[#{delim}] */n).
|
63
|
+
inject({}) { |hash, param| key, val = unescape_uri(param).split('=',2)
|
64
|
+
hash.update(key.split(/[\]\[]+/).reverse.
|
65
|
+
inject(val) { |x,i| Hash[i,x] }, &m)
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
module ViewHelper
|
71
|
+
# truncates the string at the first space after +len+ characters
|
72
|
+
def truncate(str, len = 80)
|
73
|
+
if str and str.length > len and (add = str[len..-1].index(' '))
|
74
|
+
str = str[0, len + add] + '…'
|
75
|
+
end
|
76
|
+
str
|
77
|
+
end
|
78
|
+
|
79
|
+
def tick_or_cross(t)
|
80
|
+
"<img src=\"/s/i/#{t ?'tick':'cross'}.png\" alt=\"#{t ?'yes':'no'}\"/>"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class Controller
|
85
|
+
include ViewHelper
|
86
|
+
APP_DIR = File.expand_path(File.join(File.dirname(__FILE__), "browser/"))
|
87
|
+
STATIC_DIR = File.expand_path(File.join(APP_DIR, "s/"))
|
88
|
+
|
89
|
+
def initialize(reader, path, vars)
|
90
|
+
@reader = reader
|
91
|
+
@path = path
|
92
|
+
vars.each_pair {|key, val| instance_eval("@#{key} = val")}
|
93
|
+
@controller_path = pathify(self.class.to_s.gsub(/.*:/, ''))
|
94
|
+
end
|
95
|
+
|
96
|
+
def method_missing(meth_id, *args)
|
97
|
+
render(:action => meth_id)
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def load_page(page)
|
103
|
+
File.read(File.join(APP_DIR, page))
|
104
|
+
end
|
105
|
+
|
106
|
+
def render(options = {})
|
107
|
+
options = {
|
108
|
+
:controller => @controller_path,
|
109
|
+
:action => :index,
|
110
|
+
:status => 200,
|
111
|
+
:content_type => 'text/html',
|
112
|
+
:env => nil,
|
113
|
+
:layout => 'views/layout.rhtml',
|
114
|
+
}.update(options)
|
115
|
+
|
116
|
+
path = "views/#{options[:controller]}/#{options[:action]}.rhtml"
|
117
|
+
content = ERB.new(load_page(path)).result(lambda{})
|
118
|
+
if options[:layout]
|
119
|
+
content = ERB.new(load_page(options[:layout])).result(lambda{})
|
120
|
+
end
|
121
|
+
|
122
|
+
return options[:status], options[:content_type], content
|
123
|
+
end
|
124
|
+
|
125
|
+
# takes an optional block to set optional attributes in the links
|
126
|
+
def paginate(idx, max, url, &b)
|
127
|
+
return '' if max == 0
|
128
|
+
url = url.gsub(%r{^/?(.*?)/?$}, '\1')
|
129
|
+
b ||= lambda{}
|
130
|
+
link = lambda {|*args|
|
131
|
+
i, title, text = args
|
132
|
+
"<a href=\"/#{url}/#{i}#{'?' + @query_string if @query_string}\" " +
|
133
|
+
"#{'onclick="return false;"' if (i == idx)} " +
|
134
|
+
"class=\"#{'disabled ' if (i == idx)}#{b.call(i)}\" " +
|
135
|
+
"title=\"#{title||"Go to page #{i}"}\">#{text||i}</a>"
|
136
|
+
}
|
137
|
+
res = '<div class="nav">'
|
138
|
+
if (idx > 0)
|
139
|
+
res << link.call(idx - 1, "Go to previous page", "« Previous")
|
140
|
+
else
|
141
|
+
res << "<a href=\"/#{url}/0\" onclick=\"return false;\" " +
|
142
|
+
"class=\"disabled\" title=\"Disabled\">« Previous</a>"
|
143
|
+
end
|
144
|
+
if idx < 10
|
145
|
+
idx.times {|i| res << link.call(i)}
|
146
|
+
else
|
147
|
+
(0..2).each {|i| res << link.call(i)}
|
148
|
+
res << ' … '
|
149
|
+
((idx-4)...idx).each {|i| res << link.call(i)}
|
150
|
+
end
|
151
|
+
res << link.call(idx, 'Current Page')
|
152
|
+
if idx > (max - 10)
|
153
|
+
((idx+1)...max).each {|i| res << link.call(i)}
|
154
|
+
else
|
155
|
+
((idx+1)..(idx+4)).each {|i| res << link.call(i)}
|
156
|
+
res << ' … '
|
157
|
+
((max-3)...max).each {|i| res << link.call(i)}
|
158
|
+
end
|
159
|
+
if (idx < (max - 1))
|
160
|
+
res << link.call(idx + 1, "Go to next page", "Next »")
|
161
|
+
else
|
162
|
+
res << "<a href=\"/#{url}/#{max-1}\" onclick=\"return false;\" " +
|
163
|
+
"class=\"disabled\" title=\"Disabled\"}\">Next »</a>"
|
164
|
+
end
|
165
|
+
res << '</div>'
|
166
|
+
end
|
167
|
+
private
|
168
|
+
|
169
|
+
def pathify(str)
|
170
|
+
str.gsub(/Controller$/, '').gsub(/([a-z])([A-Z])/) {"#{$1}-#{$2}"}.downcase
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class ErrorController < Controller
|
175
|
+
def index
|
176
|
+
render(:status => 404)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
class HomeController < Controller
|
181
|
+
end
|
182
|
+
|
183
|
+
class DocumentController < Controller
|
184
|
+
def list(page = 0)
|
185
|
+
@page = (page||0).to_i
|
186
|
+
@page_size = @params[:page_size]||10
|
187
|
+
@first = @page * @page_size
|
188
|
+
@last = [@reader.max_doc, (@page + 1) * @page_size].min
|
189
|
+
render(:action => :list)
|
190
|
+
end
|
191
|
+
alias :index :list
|
192
|
+
|
193
|
+
def show(doc_id)
|
194
|
+
doc_id = @params['doc_id']||doc_id||'0'
|
195
|
+
if doc_id !~ /^\d+$/
|
196
|
+
raise ArgumentError.new("invalid document number '#{doc_id}'")
|
197
|
+
end
|
198
|
+
@doc_id = doc_id.to_i
|
199
|
+
@doc = @reader[@doc_id].load unless @reader.deleted?(@doc_id)
|
200
|
+
render(:action => :show)
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
def choose_document(doc_id='')
|
206
|
+
<<-EOF
|
207
|
+
<form action="" method="get" onsubmit="location.href='/document/show/' + document.getElementById('doc_id').value;return false;">
|
208
|
+
<label for="doc_id">Go to document:
|
209
|
+
<input type="text" id="doc_id" name="doc_id" size="4" value="#{@doc_id}"/>
|
210
|
+
</label>
|
211
|
+
</form>
|
212
|
+
EOF
|
213
|
+
end
|
214
|
+
|
215
|
+
def paginate_docs
|
216
|
+
paginate(@doc_id, @reader.max_doc, '/document/show/') {|i|
|
217
|
+
'deleted' if @reader.deleted?(i)
|
218
|
+
}
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
class TermController < Controller
|
223
|
+
def show(field)
|
224
|
+
if field and field.length > 0
|
225
|
+
@field = field.to_sym
|
226
|
+
@terms = @reader.terms(@field).to_json(:fast)
|
227
|
+
end
|
228
|
+
render(:action => :index)
|
229
|
+
end
|
230
|
+
|
231
|
+
def termdocs(args)
|
232
|
+
args = args.split('/')
|
233
|
+
@field = args.shift.intern
|
234
|
+
@term = args.join('/')
|
235
|
+
render(:action => :termdocs,
|
236
|
+
:content_type => 'text/plain',
|
237
|
+
:layout => false)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
class TermVectorController < Controller
|
242
|
+
end
|
243
|
+
|
244
|
+
class HelpController < Controller
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
function AutoSuggestControl(oTextbox, oProvider) {
|
2
|
+
this.cur = -1;
|
3
|
+
this.layer = null;
|
4
|
+
this.provider = oProvider;
|
5
|
+
this.textbox = oTextbox;
|
6
|
+
this.init();
|
7
|
+
}
|
8
|
+
AutoSuggestControl.prototype.init = function() {
|
9
|
+
var oThis = this;
|
10
|
+
this.textbox.setAttribute("autocomplete", "off");
|
11
|
+
this.textbox.onkeyup = function(oEvent) {
|
12
|
+
if (!oEvent) {
|
13
|
+
oEvent = window.event;
|
14
|
+
}
|
15
|
+
oThis.handleKeyUp(oEvent);
|
16
|
+
};
|
17
|
+
this.textbox.onkeydown = function (oEvent) {
|
18
|
+
if (!oEvent) {
|
19
|
+
oEvent = window.event;
|
20
|
+
}
|
21
|
+
oThis.handleKeyDown(oEvent);
|
22
|
+
};
|
23
|
+
this.textbox.onblur = function () {
|
24
|
+
oThis.hideSuggestions();
|
25
|
+
};
|
26
|
+
|
27
|
+
this.createDropDown();
|
28
|
+
};
|
29
|
+
AutoSuggestControl.prototype.selectRange = function(iStart, iLength) {
|
30
|
+
if (this.textbox.createTextRange) {
|
31
|
+
var oRange = this.textbox.createTextRange();
|
32
|
+
oRange.moveStart("character", iStart);
|
33
|
+
oRange.moveEnd("character", iLength - this.textbox.value.length);
|
34
|
+
oRange.select();
|
35
|
+
} else if (this.textbox.setSelectionRange) {
|
36
|
+
this.textbox.setSelectionRange(iStart, iLength);
|
37
|
+
}
|
38
|
+
this.textbox.focus();
|
39
|
+
};
|
40
|
+
AutoSuggestControl.prototype.typeAhead = function(sSuggestion) {
|
41
|
+
if (this.textbox.createTextRange || this.textbox.setSelectionRange) {
|
42
|
+
var iLen = this.textbox.value.length;
|
43
|
+
this.textbox.value = sSuggestion;
|
44
|
+
this.selectRange(iLen, sSuggestion.length);
|
45
|
+
}
|
46
|
+
};
|
47
|
+
AutoSuggestControl.prototype.autosuggest = function(aSuggestions, bTypeAhead) {
|
48
|
+
if (aSuggestions.length > 0) {
|
49
|
+
if (bTypeAhead) {
|
50
|
+
this.typeAhead(aSuggestions[0]);
|
51
|
+
}
|
52
|
+
this.showSuggestions(aSuggestions);
|
53
|
+
} else {
|
54
|
+
this.hideSuggestions();
|
55
|
+
}
|
56
|
+
};
|
57
|
+
AutoSuggestControl.prototype.handleKeyUp = function(oEvent) {
|
58
|
+
var iKeyCode = oEvent.keyCode;
|
59
|
+
|
60
|
+
if (iKeyCode == 8 || iKeyCode == 46) {
|
61
|
+
this.provider.requestSuggestions(this, false);
|
62
|
+
|
63
|
+
} else if (iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode <= 46) || (iKeyCode >= 112 && iKeyCode <= 123)) {
|
64
|
+
//ignore
|
65
|
+
} else {
|
66
|
+
this.provider.requestSuggestions(this, true);
|
67
|
+
}
|
68
|
+
};
|
69
|
+
AutoSuggestControl.prototype.handleKeyDown = function (oEvent) {
|
70
|
+
switch(oEvent.keyCode) {
|
71
|
+
case 38: //up arrow
|
72
|
+
this.previousSuggestion();
|
73
|
+
break;
|
74
|
+
case 40: //down arrow
|
75
|
+
this.nextSuggestion();
|
76
|
+
break;
|
77
|
+
case 13: //enter
|
78
|
+
this.hideSuggestions();
|
79
|
+
break;
|
80
|
+
}
|
81
|
+
};
|
82
|
+
AutoSuggestControl.prototype.hideSuggestions = function() {
|
83
|
+
this.layer.style.visibility = "hidden";
|
84
|
+
};
|
85
|
+
AutoSuggestControl.prototype.highlightSuggestion = function(oSuggestionNode) {
|
86
|
+
for (var i=0; i < this.layer.childNodes.length; i++) {
|
87
|
+
var oNode = this.layer.childNodes[i];
|
88
|
+
if (oNode == oSuggestionNode) {
|
89
|
+
oNode.className = "current"
|
90
|
+
} else if (oNode.className == "current") {
|
91
|
+
oNode.className = "";
|
92
|
+
}
|
93
|
+
}
|
94
|
+
};
|
95
|
+
AutoSuggestControl.prototype.createDropDown = function() {
|
96
|
+
|
97
|
+
this.layer = document.createElement("div");
|
98
|
+
this.layer.className = "suggestions";
|
99
|
+
this.layer.style.visibility = "hidden";
|
100
|
+
this.layer.style.width = this.textbox.offsetWidth;
|
101
|
+
document.body.appendChild(this.layer);
|
102
|
+
|
103
|
+
var oThis = this;
|
104
|
+
|
105
|
+
this.layer.onmousedown = this.layer.onmouseup =
|
106
|
+
this.layer.onmouseover = function(oEvent) {
|
107
|
+
oEvent = oEvent || window.event;
|
108
|
+
oTarget = oEvent.target || oEvent.srcElement;
|
109
|
+
|
110
|
+
if (oEvent.type == "mousedown") {
|
111
|
+
oThis.textbox.value = oTarget.firstChild.nodeValue;
|
112
|
+
oThis.hideSuggestions();
|
113
|
+
} else if (oEvent.type == "mouseover") {
|
114
|
+
oThis.highlightSuggestion(oTarget);
|
115
|
+
} else {
|
116
|
+
oThis.textbox.focus();
|
117
|
+
}
|
118
|
+
};
|
119
|
+
};
|
120
|
+
AutoSuggestControl.prototype.getLeft = function() {
|
121
|
+
|
122
|
+
var oNode = this.textbox;
|
123
|
+
var iLeft = 0;
|
124
|
+
|
125
|
+
while (oNode.tagName != "BODY") {
|
126
|
+
iLeft += oNode.offsetLeft;
|
127
|
+
oNode = oNode.offsetParent;
|
128
|
+
}
|
129
|
+
|
130
|
+
return iLeft;
|
131
|
+
};
|
132
|
+
AutoSuggestControl.prototype.getTop = function() {
|
133
|
+
|
134
|
+
var oNode = this.textbox;
|
135
|
+
var iTop = 0;
|
136
|
+
|
137
|
+
while (oNode.tagName != "BODY") {
|
138
|
+
iTop += oNode.offsetTop;
|
139
|
+
oNode = oNode.offsetParent;
|
140
|
+
}
|
141
|
+
|
142
|
+
return iTop;
|
143
|
+
};
|
144
|
+
AutoSuggestControl.prototype.showSuggestions = function(aSuggestions) {
|
145
|
+
|
146
|
+
var oDiv = null;
|
147
|
+
this.layer.innerHTML = "";
|
148
|
+
|
149
|
+
for (var i = 0; i < aSuggestions.length; i++) {
|
150
|
+
oDiv = document.createElement("div");
|
151
|
+
oDiv.appendChild(document.createTextNode(aSuggestions[i]));
|
152
|
+
this.layer.appendChild(oDiv);
|
153
|
+
}
|
154
|
+
|
155
|
+
this.layer.style.left = this.getLeft() + "px";
|
156
|
+
this.layer.style.top = (this.getTop()+this.textbox.offsetHeight) + "px";
|
157
|
+
this.layer.style.width = this.textbox.offsetWidth + "px";
|
158
|
+
this.layer.style.visibility = "visible";
|
159
|
+
};
|
160
|
+
AutoSuggestControl.prototype.nextSuggestion = function() {
|
161
|
+
var cSuggestionNodes = this.layer.childNodes;
|
162
|
+
|
163
|
+
if (cSuggestionNodes.length > 0 && this.cur < cSuggestionNodes.length-1) {
|
164
|
+
var oNode = cSuggestionNodes[++this.cur];
|
165
|
+
this.highlightSuggestion(oNode);
|
166
|
+
this.textbox.value = oNode.firstChild.nodeValue;
|
167
|
+
}
|
168
|
+
};
|
169
|
+
AutoSuggestControl.prototype.previousSuggestion = function() {
|
170
|
+
var cSuggestionNodes = this.layer.childNodes;
|
171
|
+
|
172
|
+
if (cSuggestionNodes.length > 0 && this.cur > 0) {
|
173
|
+
var oNode = cSuggestionNodes[--this.cur];
|
174
|
+
this.highlightSuggestion(oNode);
|
175
|
+
this.textbox.value = oNode.firstChild.nodeValue;
|
176
|
+
}
|
177
|
+
};
|
178
|
+
|
179
|
+
function bsearch(aArray, item, less_than) {
|
180
|
+
var left = -1,
|
181
|
+
right = aArray.length,
|
182
|
+
mid;
|
183
|
+
while (right > left + 1) {
|
184
|
+
mid = (left + right) >>> 1;
|
185
|
+
if (less_than(aArray[mid], item)) {
|
186
|
+
left = mid;
|
187
|
+
} else {
|
188
|
+
right = mid;
|
189
|
+
}
|
190
|
+
}
|
191
|
+
return right;
|
192
|
+
}
|