weft-qda 0.9.6 → 0.9.8
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/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
|