ferret 0.11.3 → 0.11.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.
- 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
|
+
}
|