weft-qda 0.9.6 → 0.9.8
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/weft.rb +16 -1
- data/lib/weft/WEFT-VERSION-STRING.rb +1 -1
- data/lib/weft/application.rb +17 -74
- data/lib/weft/backend.rb +6 -32
- data/lib/weft/backend/sqlite.rb +222 -164
- data/lib/weft/backend/sqlite/category_tree.rb +52 -48
- data/lib/weft/backend/sqlite/database.rb +57 -0
- data/lib/weft/backend/sqlite/upgradeable.rb +7 -0
- data/lib/weft/broadcaster.rb +90 -0
- data/lib/weft/category.rb +139 -47
- data/lib/weft/codereview.rb +160 -0
- data/lib/weft/coding.rb +74 -23
- data/lib/weft/document.rb +23 -10
- data/lib/weft/exceptions.rb +10 -0
- data/lib/weft/filters.rb +47 -224
- data/lib/weft/filters/indexers.rb +137 -0
- data/lib/weft/filters/input.rb +118 -0
- data/lib/weft/filters/output.rb +101 -0
- data/lib/weft/filters/templates.rb +80 -0
- data/lib/weft/filters/win32backtick.rb +246 -0
- data/lib/weft/query.rb +169 -0
- data/lib/weft/wxgui.rb +349 -294
- data/lib/weft/wxgui/constants.rb +43 -0
- data/lib/weft/wxgui/controls.rb +6 -0
- data/lib/weft/wxgui/controls/category_dropdown.rb +192 -0
- data/lib/weft/wxgui/controls/category_tree.rb +314 -0
- data/lib/weft/wxgui/controls/document_list.rb +97 -0
- data/lib/weft/wxgui/controls/multitype_control.rb +37 -0
- data/lib/weft/wxgui/{inspectors → controls}/textcontrols.rb +235 -64
- data/lib/weft/wxgui/dialogs.rb +144 -41
- data/lib/weft/wxgui/error_handler.rb +116 -36
- data/lib/weft/wxgui/exceptions.rb +7 -0
- data/lib/weft/wxgui/inspectors.rb +61 -208
- data/lib/weft/wxgui/inspectors/category.rb +19 -16
- data/lib/weft/wxgui/inspectors/codereview.rb +90 -132
- data/lib/weft/wxgui/inspectors/document.rb +12 -8
- data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -56
- data/lib/weft/wxgui/inspectors/query.rb +284 -0
- data/lib/weft/wxgui/inspectors/script.rb +147 -23
- data/lib/weft/wxgui/lang/en.rb +69 -0
- data/lib/weft/wxgui/sidebar.rb +90 -432
- data/lib/weft/wxgui/utilities.rb +70 -91
- data/lib/weft/wxgui/workarea.rb +150 -43
- data/share/icons/category.ico +0 -0
- data/share/icons/category.xpm +109 -0
- data/share/icons/codereview.ico +0 -0
- data/share/icons/codereview.xpm +54 -0
- data/share/icons/d_and_c.xpm +126 -0
- data/share/icons/document.ico +0 -0
- data/share/icons/document.xpm +70 -0
- data/share/icons/project.ico +0 -0
- data/share/icons/query.ico +0 -0
- data/share/icons/query.xpm +56 -0
- data/{lib/weft/wxgui → share/icons}/search.xpm +0 -0
- data/share/icons/weft.ico +0 -0
- data/share/icons/weft.xpm +62 -0
- data/share/icons/weft16.ico +0 -0
- data/share/icons/weft32.ico +0 -0
- data/share/templates/category_plain.html +18 -0
- data/share/templates/codereview_plain.html +18 -0
- data/share/templates/document_plain.html +13 -0
- data/share/templates/document_plain.txt +7 -0
- data/test/001-document.rb +55 -36
- data/test/002-category.rb +81 -6
- data/test/003-code.rb +8 -4
- data/test/004-application.rb +13 -34
- data/test/005-query_review.rb +139 -0
- data/test/006-filters.rb +54 -42
- data/test/007-output_filters.rb +113 -0
- data/test/009a-backend_sqlite_basic.rb +95 -24
- data/test/009b-backend_sqlite_complex.rb +43 -62
- data/test/009c_backend_sqlite_bench.rb +5 -10
- data/test/053-doc_inspector.rb +46 -0
- data/test/055-query_window.rb +50 -0
- data/test/all-tests.rb +1 -0
- data/test/test-common.rb +19 -0
- data/test/testdata/empty.qdp +0 -0
- data/test/testdata/simple with space.pdf +0 -0
- data/test/testdata/simple.pdf +0 -0
- data/weft-qda.rb +40 -7
- metadata +74 -14
- data/lib/weft/wxgui/category.xpm +0 -26
- data/lib/weft/wxgui/document.xpm +0 -25
- data/lib/weft/wxgui/inspectors/search.rb +0 -265
- data/lib/weft/wxgui/mondrian.xpm +0 -44
- data/lib/weft/wxgui/weft16.xpm +0 -31
@@ -0,0 +1,137 @@
|
|
1
|
+
module QDA
|
2
|
+
class Indexer
|
3
|
+
attr_reader :cursor
|
4
|
+
def initialize()
|
5
|
+
@cursor = 0
|
6
|
+
end
|
7
|
+
|
8
|
+
def index(str)
|
9
|
+
prepare(str)
|
10
|
+
str.each_line { | line | feed(line) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def terminate()
|
14
|
+
end
|
15
|
+
|
16
|
+
def prepare(content)
|
17
|
+
end
|
18
|
+
|
19
|
+
def feed(line)
|
20
|
+
@cursor += line.length
|
21
|
+
end
|
22
|
+
end
|
23
|
+
# An indexer which records the position of words for later reverse
|
24
|
+
# retrieval
|
25
|
+
class WordIndexer < Indexer
|
26
|
+
attr_reader :words
|
27
|
+
# includes accented latin-1 characters
|
28
|
+
WORD_TOKENIZER = /[\w\xC0-\xD6\xD8-\xF6\xF8-\xFF][\w\xC0-\xD6\xD8-\xF6\xF8-\xFF\']+/
|
29
|
+
def initialize()
|
30
|
+
super
|
31
|
+
@words = Hash.new { | h, k | h[k] = [] }
|
32
|
+
end
|
33
|
+
|
34
|
+
def feed(line)
|
35
|
+
line.scan( WORD_TOKENIZER ) do | word |
|
36
|
+
next if word.length == 1
|
37
|
+
if word.respond_to?(:offset)
|
38
|
+
offset = cursor + word.offset
|
39
|
+
else
|
40
|
+
offset = cursor + Regexp.last_match.begin(0)
|
41
|
+
end
|
42
|
+
@words[word.to_s].push(offset)
|
43
|
+
end
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# An indexer that uses text patterns to identify, for example,
|
49
|
+
# passages by a particular speaker, or text headings.
|
50
|
+
# The indexer can recognise a number of different types of codes,
|
51
|
+
# each denoted by a pattern of punctuation in a line of text. A
|
52
|
+
# default coder recognises the following
|
53
|
+
# A 'Heading', marked by a line **NAME OF HEADING**
|
54
|
+
# A 'Speaker', marked by a line SpeakerName:
|
55
|
+
#
|
56
|
+
# After the filter has run, the results of the coding can be
|
57
|
+
# retrieved by calling Autocoder#codes
|
58
|
+
# This is a hash of codetype names to inner hashes of codevalue names
|
59
|
+
# (strings) to QDA::Codesets corresponding to them.
|
60
|
+
class AutoCoder < Indexer
|
61
|
+
STANDARD_TRIGGER_RULES = {
|
62
|
+
/^(\w+)\:\s*$/ => 'Speaker',
|
63
|
+
/^\*\*(.*)\*\*$/ => 'Heading'
|
64
|
+
}
|
65
|
+
|
66
|
+
attr_reader :codes
|
67
|
+
# +rules+ should be a hash of string keys, naming types of autocode
|
68
|
+
# (e.g. "Speaker", "Heading", "Topic") mapped to values, which
|
69
|
+
# should be regular expressions specifying how the start of such a
|
70
|
+
# code should be recognised.
|
71
|
+
# For example, to find topics marked by the characters '##' at the
|
72
|
+
# start of the line:
|
73
|
+
# 'Heading' => /^##(.*)$/
|
74
|
+
def initialize(rules = STANDARD_TRIGGER_RULES)
|
75
|
+
super()
|
76
|
+
@trigger_rules = rules
|
77
|
+
@codes = Hash.new { | h, k | h[k] = {} }
|
78
|
+
@curr_codes = {}
|
79
|
+
end
|
80
|
+
|
81
|
+
# check a line of document content for triggers
|
82
|
+
def feed(line)
|
83
|
+
@trigger_rules.each do | rule, type |
|
84
|
+
if match = rule.match(line)
|
85
|
+
trigger(cursor, type, match[1])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
super
|
89
|
+
end
|
90
|
+
|
91
|
+
# take action on finding a autocode marker
|
92
|
+
def trigger(cursor, group, codename)
|
93
|
+
# save any previous code that was being done for this group
|
94
|
+
store(group) if @curr_codes[group]
|
95
|
+
new_codeset = get_code(group, codename)
|
96
|
+
@curr_codes[group] = [ new_codeset, cursor ]
|
97
|
+
end
|
98
|
+
private :trigger
|
99
|
+
|
100
|
+
# returns the code name +codename+ within the group +group+,
|
101
|
+
# creating a new empty category
|
102
|
+
def get_code(group, codename)
|
103
|
+
return @codes[group][codename] if @codes[group][codename]
|
104
|
+
@codes[group][codename] = QDA::CodeSet.new()
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns the names and codesets for autocodes in group +group+
|
108
|
+
# in a series of pairs
|
109
|
+
def each_autocode(group)
|
110
|
+
@codes[group].each { | name, codeset | yield name, codeset }
|
111
|
+
end
|
112
|
+
|
113
|
+
# alters all the stored coding in this autocoder so that it refers
|
114
|
+
# to the document identified by +docid+
|
115
|
+
def apply(docid)
|
116
|
+
@codes.values.each do | group |
|
117
|
+
group.values.each do | codeset |
|
118
|
+
codeset.map! { | x | x.docid = docid; x }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# finish up all currently active coding in this autocoder
|
124
|
+
def terminate()
|
125
|
+
@curr_codes.each_key { | group | store(group) }
|
126
|
+
end
|
127
|
+
|
128
|
+
# finish the coding for the current code being used among +group+
|
129
|
+
def store(group)
|
130
|
+
codeset, start = @curr_codes[group]
|
131
|
+
# -1 here is a placeholder
|
132
|
+
terminus = cursor - start
|
133
|
+
codeset.add( Code.new(-1, start, terminus) )
|
134
|
+
end
|
135
|
+
private :store
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module QDA
|
2
|
+
module Filters
|
3
|
+
class DocumentTextFilter
|
4
|
+
IMPORT_CLASS = Document
|
5
|
+
MEDIA_TYPE = 'text/plain'
|
6
|
+
|
7
|
+
def run(content_or_file)
|
8
|
+
content = respond_to?(:read) ? read(content_or_file) :
|
9
|
+
content_or_file
|
10
|
+
doc = QDA::Document.new('', '')
|
11
|
+
# signal to indexers we're about to start
|
12
|
+
@indexers.each { | indexer | indexer.prepare(content) }
|
13
|
+
content.each_line do | line |
|
14
|
+
doc.append(line.to_s.chomp)
|
15
|
+
@indexers.each { | indexer | indexer.feed(line) }
|
16
|
+
end
|
17
|
+
@indexers.each { | indexer | indexer.terminate() }
|
18
|
+
doc.create
|
19
|
+
doc
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
attr_reader :cursor
|
24
|
+
|
25
|
+
def initialize()
|
26
|
+
@cursor = 0
|
27
|
+
@indexers = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_indexer(indexer)
|
31
|
+
unless indexer.respond_to?(:feed)
|
32
|
+
raise "Document indexers should have a feed method"
|
33
|
+
end
|
34
|
+
@indexers.push(indexer)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module TextReader
|
39
|
+
def read(filename)
|
40
|
+
File.read(filename)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class TextFileDocumentFilter < DocumentTextFilter
|
45
|
+
EXTENSIONS = ['txt']
|
46
|
+
DESCRIPTION = 'Plain Text Files'
|
47
|
+
include TextReader
|
48
|
+
Filters.register_filter(self)
|
49
|
+
end
|
50
|
+
|
51
|
+
module PDFReader
|
52
|
+
# This tests if we are on windows - will raise an exception if used
|
53
|
+
# on *nix platforms
|
54
|
+
begin
|
55
|
+
require 'weft/filters/win32backtick.rb'
|
56
|
+
rescue LoadError
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
maybe_pdf = 'pdftotext'
|
61
|
+
begin
|
62
|
+
if defined? Win32Process
|
63
|
+
# the -v option for pdftotext writes to STDERR
|
64
|
+
ignore, test = Win32Process.backtick("#{maybe_pdf} -v")
|
65
|
+
else
|
66
|
+
test = `#{maybe_pdf} -v 2>&1`
|
67
|
+
end
|
68
|
+
|
69
|
+
if test =~ /pdftotext version 3/
|
70
|
+
PDF_TO_TEXT_EXEC = maybe_pdf
|
71
|
+
else
|
72
|
+
warn 'PDFtotext Version 3 not found in path; ' <<
|
73
|
+
'PDF Filters will not be available'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def read(file)
|
78
|
+
file = "\"#{file}\"" if file =~ /\s/
|
79
|
+
cmd = "#{PDF_TO_TEXT_EXEC} -nopgbrk #{file} -"
|
80
|
+
if defined? Win32Process
|
81
|
+
do_read_win32(cmd)
|
82
|
+
else
|
83
|
+
do_read_unixy(cmd)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def do_read_win32(cmd)
|
89
|
+
out, err = Win32Process.backtick(cmd)
|
90
|
+
return out unless out.empty?
|
91
|
+
if err =~ /Copying of text from this document is not allowed/
|
92
|
+
raise IOError.new("Could not extract PDF text: this PDF is locked")
|
93
|
+
else
|
94
|
+
raise IOError.new("Could not extract PDF text: #{err}")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def do_read_unixy(cmd)
|
99
|
+
content = `#{cmd} 2>&1`
|
100
|
+
case $CHILD_STATUS
|
101
|
+
when 0
|
102
|
+
return content
|
103
|
+
when 3
|
104
|
+
raise IOError.new("Could not extract PDF text: this PDF is locked")
|
105
|
+
else
|
106
|
+
raise IOError.new("Could not extract PDF text: #{content}")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class PDFFileDocumentFilter < DocumentTextFilter
|
112
|
+
EXTENSIONS = ['pdf']
|
113
|
+
DESCRIPTION = 'PDF Files'
|
114
|
+
include PDFReader
|
115
|
+
Filters.register_filter(self) if defined?(PDFReader::PDF_TO_TEXT_EXEC)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module QDA
|
2
|
+
module Filters
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'csv'
|
6
|
+
rescue LoadError
|
7
|
+
end
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'PageTemplate/parser'
|
11
|
+
require 'PageTemplate/commands'
|
12
|
+
require 'weft/filters/templates.rb'
|
13
|
+
# These features won't be used if not available
|
14
|
+
rescue LoadError
|
15
|
+
end
|
16
|
+
|
17
|
+
class OutputFilter
|
18
|
+
def initialize(app = nil)
|
19
|
+
@app = app
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Inherit, don't instantiate this class directly
|
24
|
+
class DocumentOutputFilter < OutputFilter
|
25
|
+
EXPORT_CLASS = Document
|
26
|
+
def DocumentOutputFilter.def_name(doc)
|
27
|
+
'document-' << doc.title.gsub(/\s+/, '_').gsub(/\W/,'')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# A filter that formats a Category for output, including supplying the
|
32
|
+
# text marked by the category
|
33
|
+
class CategoryOutputFilter < OutputFilter
|
34
|
+
EXPORT_CLASS = Category
|
35
|
+
def CategoryOutputFilter.def_name(cat)
|
36
|
+
'category-' << cat.name.gsub(/\s+/, '_').gsub(/\W/,'')
|
37
|
+
end
|
38
|
+
|
39
|
+
def variables(obj)
|
40
|
+
{ 'text' => @app.get_text_at_category(obj) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class CodeReviewOutputFilter < OutputFilter
|
45
|
+
EXPORT_CLASS = CodeReview
|
46
|
+
def self.def_name(cr)
|
47
|
+
'codereview-' << cr.dbid.to_s
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class CodeReviewCSVOutput < CodeReviewOutputFilter
|
52
|
+
EXTENSION = 'csv'
|
53
|
+
DESCRIPTION = 'Code Review as spreadsheet'
|
54
|
+
def write(cr, file = STDOUT, mode = nil )
|
55
|
+
mode ||= cr.count_method
|
56
|
+
CSV::Writer.generate(file) do | csv |
|
57
|
+
cr.output_rows.each { | r | csv << r }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
Filters.register_filter(self) if defined?(CSV)
|
61
|
+
end
|
62
|
+
|
63
|
+
tfiles = File.join(TemplatedOutput::TEMPLATES_DIR, '*.*')
|
64
|
+
|
65
|
+
# autoload template-based output filters from the shared templates dir
|
66
|
+
# this is located in /path/to/weft/share/templates.
|
67
|
+
# Each of these special template files should contain a number of comment
|
68
|
+
# header lines, followed by a text template.
|
69
|
+
# Specifications for the template are given as comment lines as follows:
|
70
|
+
# DESCRIPTION [required] : short description of template's use, for UI
|
71
|
+
# EXPORT_CLASS [required] : QDA class templated by this file
|
72
|
+
# EXTENSION [optional ] : what file extension exported; default,
|
73
|
+
# same extension as the template file
|
74
|
+
# CLASSNAME [ optional ] : make a reference to this class in the module
|
75
|
+
# QDA::Filters
|
76
|
+
Dir.glob( tfiles ).each do | tfile |
|
77
|
+
tpl_class = nil
|
78
|
+
File.foreach(tfile) do | line |
|
79
|
+
case line
|
80
|
+
when /^#\s*EXPORT_CLASS\s*:(.*)$/
|
81
|
+
base_class = Filters.const_get( $1.strip << 'OutputFilter' )
|
82
|
+
tpl_class = Class.new( base_class )
|
83
|
+
when /^#\s*(DESCRIPTION|EXTENSION)\s*:(.*)$/
|
84
|
+
tpl_class.const_set($1.strip, $2)
|
85
|
+
when /^#\s*CLASSNAME\s*:(.*)$/
|
86
|
+
Filters.const_set($1.strip, tpl_class)
|
87
|
+
when /^#/
|
88
|
+
# ignore
|
89
|
+
else
|
90
|
+
break
|
91
|
+
end
|
92
|
+
end
|
93
|
+
next unless tpl_class
|
94
|
+
tpl_class.const_set( 'TEMPLATE_FILE', File.basename(tfile) )
|
95
|
+
if not defined? tpl_class.EXTENSION
|
96
|
+
tpl_class.const_set( 'EXTENSION', File.extname(tfile) )
|
97
|
+
end
|
98
|
+
tpl_class.instance_eval { include TemplatedOutput }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module QDA::Filters
|
2
|
+
module Templates
|
3
|
+
class CommentStrippingFileSource < PageTemplate::FileSource
|
4
|
+
# return the template content of the file named +name+. Uses the standard
|
5
|
+
# PageTemplate FileSource mechanism for searching directories etc, but
|
6
|
+
# removes any comment lines (all lines at the beginning of the document
|
7
|
+
# starting with a comment marker '#') before returning the content.
|
8
|
+
def get(name)
|
9
|
+
# all stuff starting from the beginning up to the first newline that is
|
10
|
+
# not followed by a comment character
|
11
|
+
super(name).sub(/\A.*?\n(?!#)/m, '')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ExtendedPreprocessor < PageTemplate::DefaultPreprocessor
|
16
|
+
class << self
|
17
|
+
# shorter and more ruby-ish names for the PageTemplate methods
|
18
|
+
alias :html :escapeHTML
|
19
|
+
alias :uri :escapeURI
|
20
|
+
|
21
|
+
# Takes string +string+ and splits it into HTML paragraphs, taking a blank
|
22
|
+
# line and a newline as marking the start of a paragraph.
|
23
|
+
# Does HTML escaping of the paragraph contents
|
24
|
+
def html_paras(string)
|
25
|
+
string.split(/\n\s*\n/).inject('') do | out, para |
|
26
|
+
out << "<p>" << html(para) << "</p>\n"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Included in output classes to enable them to be output via template (using
|
36
|
+
# PageTemplate under the hood).
|
37
|
+
module TemplatedOutput
|
38
|
+
include Templates
|
39
|
+
TEMPLATES_DIR = File.join( WEFT_SHAREDIR, 'templates' )
|
40
|
+
# write templated output of the QDA object +obj+ (eg a Category, a
|
41
|
+
# Document or a CodeReview) to the file or io +file+.
|
42
|
+
def write(obj, file = STDOUT)
|
43
|
+
tpl = PageTemplate::Parser.new('source' => CommentStrippingFileSource,
|
44
|
+
'include_paths' => TEMPLATES_DIR,
|
45
|
+
'preprocessor' => ExtendedPreprocessor )
|
46
|
+
tpl.load(self.class::TEMPLATE_FILE)
|
47
|
+
variables(obj).each { | k, v | tpl[k] = v }
|
48
|
+
file.puts( tpl.output() )
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a hash containing the named variables that should be templated.
|
52
|
+
# By default simply returns a hash containing the named variable 'obj'
|
53
|
+
# which contains the object being templated.
|
54
|
+
# If a variables() method is defined in the superclass (eg CategoryOutput)
|
55
|
+
# then the results from that are merged with the default. For example,
|
56
|
+
# CategoryOutput includes the named variable "text" which contains all
|
57
|
+
# the text marked by the category.
|
58
|
+
def variables(obj)
|
59
|
+
super.merge( 'obj' => obj )
|
60
|
+
rescue NoMethodError
|
61
|
+
{ 'obj' => obj }
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# Whenever this module is included in another class, it automatically
|
66
|
+
# checks whether a valid filter has been defined. If one has (by defining
|
67
|
+
# what QDA class it can output, what template file it should use, and
|
68
|
+
# what file extension it creates), then the filter is registered for use
|
69
|
+
# with the app (so it can be queried through +Filters.export_filters()+
|
70
|
+
def TemplatedOutput.included(other)
|
71
|
+
if other.is_a?(Class) and
|
72
|
+
defined?(other::EXPORT_CLASS) and
|
73
|
+
defined?(other::TEMPLATE_FILE) and
|
74
|
+
defined?(other::EXTENSION) and
|
75
|
+
defined?(PageTemplate)
|
76
|
+
QDA::Filters.register_filter(other)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'Win32API'
|
2
|
+
|
3
|
+
module QDA
|
4
|
+
module Filters
|
5
|
+
# Used only on windows to enable calling other executables without the
|
6
|
+
# annoying command-prompt box that pops up when using Ruby backticks in
|
7
|
+
# a script running under rubyw.
|
8
|
+
#
|
9
|
+
# Note - most of this code written by S Kroeger, see
|
10
|
+
# http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/155684
|
11
|
+
module Win32Process
|
12
|
+
NORMAL_PRIORITY_CLASS = 0x00000020
|
13
|
+
STARTUP_INFO_SIZE = 68
|
14
|
+
PROCESS_INFO_SIZE = 16
|
15
|
+
SECURITY_ATTRIBUTES_SIZE = 12
|
16
|
+
|
17
|
+
ERROR_SUCCESS = 0x00
|
18
|
+
FORMAT_MESSAGE_FROM_SYSTEM = 0x1000
|
19
|
+
FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x2000
|
20
|
+
|
21
|
+
HANDLE_FLAG_INHERIT = 1
|
22
|
+
HANDLE_FLAG_PROTECT_FROM_CLOSE =2
|
23
|
+
|
24
|
+
STARTF_USESHOWWINDOW = 0x00000001
|
25
|
+
STARTF_USESTDHANDLES = 0x00000100
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def raise_last_win_32_error
|
29
|
+
errorCode = Win32API.new("kernel32", "GetLastError", [], 'L').call
|
30
|
+
if errorCode != ERROR_SUCCESS
|
31
|
+
params = [
|
32
|
+
'L', # IN DWORD dwFlags,
|
33
|
+
'P', # IN LPCVOID lpSource,
|
34
|
+
'L', # IN DWORD dwMessageId,
|
35
|
+
'L', # IN DWORD dwLanguageId,
|
36
|
+
'P', # OUT LPSTR lpBuffer,
|
37
|
+
'L', # IN DWORD nSize,
|
38
|
+
'P', # IN va_list *Arguments
|
39
|
+
]
|
40
|
+
|
41
|
+
formatMessage = Win32API.new("kernel32", "FormatMessage", params, 'L')
|
42
|
+
msg = ' ' * 255
|
43
|
+
msgLength = formatMessage.call(FORMAT_MESSAGE_FROM_SYSTEM +
|
44
|
+
FORMAT_MESSAGE_ARGUMENT_ARRAY, '', errorCode, 0, msg, 255, '')
|
45
|
+
|
46
|
+
msg.gsub!(/\000/, '')
|
47
|
+
msg.strip!
|
48
|
+
raise msg
|
49
|
+
else
|
50
|
+
raise 'GetLastError returned ERROR_SUCCESS'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_pipe # returns read and write handle
|
55
|
+
params = [
|
56
|
+
'P', # pointer to read handle
|
57
|
+
'P', # pointer to write handle
|
58
|
+
'P', # pointer to security attributes
|
59
|
+
'L'] # pipe size
|
60
|
+
|
61
|
+
createPipe = Win32API.new("kernel32", "CreatePipe", params, 'I')
|
62
|
+
|
63
|
+
read_handle, write_handle = [0].pack('I'), [0].pack('I')
|
64
|
+
sec_attrs = [SECURITY_ATTRIBUTES_SIZE, 0, 1].pack('III')
|
65
|
+
|
66
|
+
raise_last_win_32_error if createPipe.Call(read_handle,
|
67
|
+
write_handle, sec_attrs, 0).zero?
|
68
|
+
|
69
|
+
[read_handle.unpack('I')[0], write_handle.unpack('I')[0]]
|
70
|
+
end
|
71
|
+
|
72
|
+
def set_handle_information(handle, flags, value)
|
73
|
+
params = [
|
74
|
+
'L', # handle to an object
|
75
|
+
'L', # specifies flags to change
|
76
|
+
'L'] # specifies new values for flags
|
77
|
+
|
78
|
+
setHandleInformation = Win32API.new("kernel32",
|
79
|
+
"SetHandleInformation", params, 'I')
|
80
|
+
raise_last_win_32_error if setHandleInformation.Call(handle,
|
81
|
+
flags, value).zero?
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def close_handle(handle)
|
86
|
+
closeHandle = Win32API.new("kernel32", "CloseHandle", ['L'], 'I')
|
87
|
+
raise_last_win_32_error if closeHandle.call(handle).zero?
|
88
|
+
end
|
89
|
+
|
90
|
+
def create_process(command, stdin, stdout, stderror)
|
91
|
+
params = [
|
92
|
+
'L', # IN LPCSTR lpApplicationName
|
93
|
+
'P', # IN LPSTR lpCommandLine
|
94
|
+
'L', # IN LPSECURITY_ATTRIBUTES lpProcessAttributes
|
95
|
+
'L', # IN LPSECURITY_ATTRIBUTES lpThreadAttributes
|
96
|
+
'L', # IN BOOL bInheritHandles
|
97
|
+
'L', # IN DWORD dwCreationFlags
|
98
|
+
'L', # IN LPVOID lpEnvironment
|
99
|
+
'L', # IN LPCSTR lpCurrentDirectory
|
100
|
+
'P', # IN LPSTARTUPINFOA lpStartupInfo
|
101
|
+
'P'] # OUT LPPROCESS_INFORMATION lpProcessInformation
|
102
|
+
|
103
|
+
startupInfo = [STARTUP_INFO_SIZE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
104
|
+
STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW, 0,
|
105
|
+
0, 0, stdin, stdout, stderror].pack('IIIIIIIIIIIISSIIII')
|
106
|
+
|
107
|
+
processInfo = [0, 0, 0, 0].pack('IIII')
|
108
|
+
command << 0
|
109
|
+
|
110
|
+
createProcess = Win32API.new("kernel32", "CreateProcess", params, 'I')
|
111
|
+
cp_args = [ 0, command, 0, 0, 1, 0, 0, 0, startupInfo, processInfo ]
|
112
|
+
raise_last_win_32_error if createProcess.call(*cp_args).zero?
|
113
|
+
|
114
|
+
hProcess, hThread,
|
115
|
+
dwProcessId, dwThreadId = processInfo.unpack('LLLL')
|
116
|
+
|
117
|
+
close_handle(hProcess)
|
118
|
+
close_handle(hThread)
|
119
|
+
|
120
|
+
[dwProcessId, dwThreadId]
|
121
|
+
end
|
122
|
+
|
123
|
+
def write_file(hFile, buffer)
|
124
|
+
params = [
|
125
|
+
'L', # handle to file to write to
|
126
|
+
'P', # pointer to data to write to file
|
127
|
+
'L', # number of bytes to write
|
128
|
+
'P', # pointer to number of bytes written
|
129
|
+
'L'] # pointer to structure for overlapped I/O
|
130
|
+
|
131
|
+
written = [0].pack('I')
|
132
|
+
writeFile = Win32API.new("kernel32", "WriteFile", params, 'I')
|
133
|
+
|
134
|
+
raise_last_win_32_error if writeFile.call(hFile, buffer, buffer.size,
|
135
|
+
written, 0).zero?
|
136
|
+
|
137
|
+
written.unpack('I')[0]
|
138
|
+
end
|
139
|
+
|
140
|
+
def read_file(hFile)
|
141
|
+
params = [
|
142
|
+
'L', # handle of file to read
|
143
|
+
'P', # pointer to buffer that receives data
|
144
|
+
'L', # number of bytes to read
|
145
|
+
'P', # pointer to number of bytes read
|
146
|
+
'L'] #pointer to structure for data
|
147
|
+
|
148
|
+
number = [0].pack('I')
|
149
|
+
buffer = ' ' * 255
|
150
|
+
|
151
|
+
readFile = Win32API.new("kernel32", "ReadFile", params, 'I')
|
152
|
+
return '' if readFile.call(hFile, buffer, 255, number, 0).zero?
|
153
|
+
|
154
|
+
buffer[0...number.unpack('I')[0]]
|
155
|
+
end
|
156
|
+
|
157
|
+
def peek_named_pipe(hFile)
|
158
|
+
params = [
|
159
|
+
'L', # handle to pipe to copy from
|
160
|
+
'L', # pointer to data buffer
|
161
|
+
'L', # size, in bytes, of data buffer
|
162
|
+
'L', # pointer to number of bytes read
|
163
|
+
'P', # pointer to total number of bytes available
|
164
|
+
'L'] # pointer to unread bytes in this message
|
165
|
+
|
166
|
+
available = [0].pack('I')
|
167
|
+
peekNamedPipe = Win32API.new("kernel32", "PeekNamedPipe", params, 'I')
|
168
|
+
|
169
|
+
return -1 if peekNamedPipe.Call(hFile, 0, 0, 0, available, 0).zero?
|
170
|
+
|
171
|
+
available.unpack('I')[0]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class Win32popenIO
|
176
|
+
def initialize (hRead, hWrite, hError)
|
177
|
+
@hRead = hRead
|
178
|
+
@hWrite = hWrite
|
179
|
+
@hError = hError
|
180
|
+
end
|
181
|
+
|
182
|
+
def write data
|
183
|
+
Win32Process::write_file(@hWrite, data.to_s)
|
184
|
+
end
|
185
|
+
|
186
|
+
def read
|
187
|
+
sleep(0.01) while Win32Process::peek_named_pipe(@hRead).zero?
|
188
|
+
Win32Process::read_file(@hRead)
|
189
|
+
end
|
190
|
+
|
191
|
+
def read_all
|
192
|
+
all = ''
|
193
|
+
until (buffer = read).empty?
|
194
|
+
all << buffer
|
195
|
+
end
|
196
|
+
all
|
197
|
+
end
|
198
|
+
|
199
|
+
def read_err
|
200
|
+
sleep(0.01) while Win32Process::peek_named_pipe(@hError).zero?
|
201
|
+
Win32Process::read_file(@hError)
|
202
|
+
end
|
203
|
+
|
204
|
+
def read_all_err
|
205
|
+
all = ''
|
206
|
+
until (buffer = read_err).empty?
|
207
|
+
all << buffer
|
208
|
+
end
|
209
|
+
all
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# The only useful public method in this class - receives a command line,
|
214
|
+
# and returns the output content and error content as a pair of strings.
|
215
|
+
# No shell expansion is carried out on the command line string.
|
216
|
+
#
|
217
|
+
# output, errors = Win32Process::backtick('ls')
|
218
|
+
def self.backtick(command)
|
219
|
+
# create 3 pipes
|
220
|
+
child_in_r, child_in_w = create_pipe
|
221
|
+
child_out_r, child_out_w = create_pipe
|
222
|
+
child_error_r, child_error_w = create_pipe
|
223
|
+
|
224
|
+
# Ensure the write handle to the pipe for STDIN is not inherited.
|
225
|
+
set_handle_information(child_in_w, HANDLE_FLAG_INHERIT, 0)
|
226
|
+
set_handle_information(child_out_r, HANDLE_FLAG_INHERIT, 0)
|
227
|
+
set_handle_information(child_error_r, HANDLE_FLAG_INHERIT, 0)
|
228
|
+
|
229
|
+
processId, threadId = create_process( command,
|
230
|
+
child_in_r,
|
231
|
+
child_out_w,
|
232
|
+
child_error_w )
|
233
|
+
# we have to close the handles, so the pipes terminate with the process
|
234
|
+
close_handle(child_in_r)
|
235
|
+
close_handle(child_out_w)
|
236
|
+
close_handle(child_error_w)
|
237
|
+
close_handle(child_in_w)
|
238
|
+
io = Win32popenIO.new(child_out_r, child_in_w, child_error_r)
|
239
|
+
|
240
|
+
out = io.read_all().gsub(/\r/, '')
|
241
|
+
err = io.read_all_err().gsub(/\r/, '')
|
242
|
+
return out, err
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|