wp2txt 2.3.2 → 2.3.3
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 +4 -4
- data/.dockerignore +3 -0
- data/.github/workflows/publish-image.yml +140 -0
- data/CHANGELOG.md +14 -0
- data/DEVELOPMENT.md +12 -4
- data/DEVELOPMENT_ja.md +2 -3
- data/README.md +1 -1
- data/README_ja.md +1 -1
- data/Rakefile +22 -16
- data/bin/wp2txt +4 -2
- data/bin/wp2txt-mcp +4 -3
- data/docs/INDEXES.md +4 -0
- data/lib/wp2txt/constants.rb +5 -0
- data/lib/wp2txt/corpus.rb +96 -79
- data/lib/wp2txt/corpus_jobs.rb +15 -13
- data/lib/wp2txt/fts_index.rb +12 -3
- data/lib/wp2txt/metadata_index.rb +3 -3
- data/lib/wp2txt/output_path.rb +72 -12
- data/lib/wp2txt/stream_processor.rb +19 -9
- data/lib/wp2txt/version.rb +1 -1
- data/spec/fts_index_spec.rb +40 -0
- data/spec/p1_correctness_spec.rb +357 -0
- data/spec/stream_processor_spec.rb +3 -1
- data/spec/titles_output_path_spec.rb +49 -1
- data/wp2txt.gemspec +1 -1
- metadata +4 -9
- data/image/wp2txt-logo.svg +0 -16
- data/image/wp2txt.svg +0 -31
- data/scripts/benchmark_regex.rb +0 -161
- data/scripts/fetch_html_entities.rb +0 -94
- data/scripts/fetch_language_metadata.rb +0 -180
- data/scripts/fetch_mediawiki_data.rb +0 -334
- data/scripts/fetch_template_data.rb +0 -186
- data/scripts/profile_memory.rb +0 -139
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "rake"
|
|
8
|
+
require_relative "../lib/wp2txt/stream_processor"
|
|
9
|
+
require_relative "../lib/wp2txt/corpus"
|
|
10
|
+
require_relative "../lib/wp2txt/corpus_jobs"
|
|
11
|
+
require_relative "../lib/wp2txt/output_path"
|
|
12
|
+
require_relative "support/multistream_fixture"
|
|
13
|
+
|
|
14
|
+
RSpec.describe "P1 correctness contracts" do
|
|
15
|
+
include MultistreamFixture
|
|
16
|
+
|
|
17
|
+
around do |example|
|
|
18
|
+
Dir.mktmpdir("wp2txt-p1-") do |dir|
|
|
19
|
+
@dir = dir
|
|
20
|
+
example.run
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def processor_for(bytes, size)
|
|
25
|
+
processor = Wp2txt::StreamProcessor.new("unused.xml", adaptive_buffer: false)
|
|
26
|
+
processor.instance_variable_set(:@file_pointer, StringIO.new(bytes.b))
|
|
27
|
+
processor.instance_variable_set(:@buffer_size, size)
|
|
28
|
+
processor
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "stream decoding" do
|
|
32
|
+
%w[é あ 😀].each do |character|
|
|
33
|
+
(1...character.bytesize).each do |boundary|
|
|
34
|
+
it "preserves #{character.inspect} split after byte #{boundary}" do
|
|
35
|
+
bytes = "x" * (8 - boundary) + character + "終"
|
|
36
|
+
processor = processor_for(bytes, 8)
|
|
37
|
+
nil while processor.send(:fill_buffer)
|
|
38
|
+
expect(processor.instance_variable_get(:@buffer)).to eq(bytes)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "scrubs invalid bytes and incomplete EOF without losing adjacent valid text" do
|
|
44
|
+
processor = processor_for("é\xFFあ😀\xE3\x81".b, 1)
|
|
45
|
+
nil while processor.send(:fill_buffer)
|
|
46
|
+
expect(processor.instance_variable_get(:@buffer)).to eq("éあ😀")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "handles empty input" do
|
|
50
|
+
processor = processor_for("", 1)
|
|
51
|
+
expect(processor.send(:fill_buffer)).to be false
|
|
52
|
+
expect(processor.instance_variable_get(:@buffer)).to eq("")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "preserves multibyte XML content through one-byte reads" do
|
|
56
|
+
path = File.join(@dir, "boundary.xml")
|
|
57
|
+
File.binwrite(path, page_xml(id: 1, ns: 0, title: "作品: é😀", text: "あé😀"))
|
|
58
|
+
processor = Wp2txt::StreamProcessor.new(path, adaptive_buffer: false)
|
|
59
|
+
processor.instance_variable_set(:@buffer_size, 1)
|
|
60
|
+
expect(processor.each_page.to_a).to eq([["作品: é😀", "あé😀"]])
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe "namespace parity" do
|
|
65
|
+
[["作品: 東京", 0, true], ["No punctuation", 4, false], ["", 6, false]].each do |title, ns, included|
|
|
66
|
+
it "uses ns=#{ns} for #{title.inspect}" do
|
|
67
|
+
xml = page_xml(id: 1, ns: ns, title: title, text: "日本語")
|
|
68
|
+
processor = Wp2txt::StreamProcessor.new("unused.xml", adaptive_buffer: false)
|
|
69
|
+
expect(!processor.send(:parse_page_xml, xml).nil?).to eq(included)
|
|
70
|
+
rows = { pages: [], categories: [], sections: [], hierarchy: [] }
|
|
71
|
+
Wp2txt::MetadataIndexBuilder.scan_page(xml, rows)
|
|
72
|
+
expect(rows[:pages].first[2]).to eq(ns) unless title.empty?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "uses namespace elements in the turbo extraction and section-stat paths" do
|
|
77
|
+
input = File.join(@dir, "pages.xml")
|
|
78
|
+
output = File.join(@dir, "output.jsonl")
|
|
79
|
+
File.write(input, [
|
|
80
|
+
page_xml(id: 1, ns: 0, title: "作品: 東京", text: "本文\n== Allowed ==\nあ"),
|
|
81
|
+
page_xml(id: 2, ns: 4, title: "No punctuation", text: "本文\n== Hidden ==\nあ"),
|
|
82
|
+
page_xml(id: 3, ns: 0, title: "Empty", text: "")
|
|
83
|
+
].join)
|
|
84
|
+
script = <<~'CODE'
|
|
85
|
+
path, input, output = ARGV
|
|
86
|
+
source = File.read(path).split("\n# Handle Ctrl+C gracefully").first
|
|
87
|
+
eval(source, TOPLEVEL_BINDING, path)
|
|
88
|
+
require File.expand_path("../lib/wp2txt/corpus", File.dirname(path))
|
|
89
|
+
app = WpApp.new
|
|
90
|
+
count = app.send(:process_xml_file_and_write, input, output,
|
|
91
|
+
Wp2txt::Corpus::RENDER_CONFIG.merge(format: :json), false, :json)
|
|
92
|
+
stats = app.send(:process_xml_file_for_stats, input)
|
|
93
|
+
puts JSON.generate(count: count, stats: stats)
|
|
94
|
+
CODE
|
|
95
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-e", script,
|
|
96
|
+
File.expand_path("../bin/wp2txt", __dir__), input, output)
|
|
97
|
+
expect(status.success?).to be(true), stderr
|
|
98
|
+
result = JSON.parse(stdout)
|
|
99
|
+
expect(result["count"]).to eq(1)
|
|
100
|
+
expect(result["stats"].to_json).to include("Allowed")
|
|
101
|
+
expect(result["stats"].to_json).not_to include("Hidden")
|
|
102
|
+
expect(File.read(output)).to include("作品: 東京")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "retains the historical ns=0 default for missing ns in both parsers" do
|
|
106
|
+
xml = page_xml(id: 1, ns: 0, title: "作品: 東京", text: "本文").sub(/<ns>.*?<\/ns>/, "")
|
|
107
|
+
processor = Wp2txt::StreamProcessor.new("unused.xml", adaptive_buffer: false)
|
|
108
|
+
expect(processor.send(:parse_page_xml, xml)).to eq(["作品: 東京", "本文"])
|
|
109
|
+
rows = { pages: [], categories: [], sections: [], hierarchy: [] }
|
|
110
|
+
Wp2txt::MetadataIndexBuilder.scan_page(xml, rows)
|
|
111
|
+
expect(rows[:pages].first[2]).to eq(0)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe "SQL rows and cells" do
|
|
116
|
+
around do |example|
|
|
117
|
+
@corpus = Wp2txt::Corpus.allocate
|
|
118
|
+
@db = SQLite3::Database.new(":memory:")
|
|
119
|
+
example.run
|
|
120
|
+
ensure
|
|
121
|
+
@db.close
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "reserves original and generated column names without losing values" do
|
|
125
|
+
path = File.join(@dir, "q.jsonl")
|
|
126
|
+
result = @corpus.send(:run_sql_file_on, @db,
|
|
127
|
+
'SELECT 1 AS x, 2 AS x, 3 AS x_2, 4 AS x, 5 AS "日本", 6 AS "日本", 7 AS ""', path)
|
|
128
|
+
record = JSON.parse(File.read(path))
|
|
129
|
+
expect(record).to eq("x" => 1, "x_3" => 2, "x_2" => 3, "x_4" => 4,
|
|
130
|
+
"日本" => 5, "日本_2" => 6, "" => 7)
|
|
131
|
+
expect(result[:column_mapping][1]).to eq(ordinal: 1, original_name: "x", output_name: "x_3")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
[0, 1, 2].each do |rows|
|
|
135
|
+
it "reports truncation only when more than the limit exists (#{rows} rows)" do
|
|
136
|
+
result = @corpus.send(:run_sql_on, @db, "SELECT 1 UNION ALL SELECT 2 LIMIT #{rows}", 1)
|
|
137
|
+
expect(result[:truncated]).to eq(rows > 1)
|
|
138
|
+
expect(result[:row_count]).to eq([rows, 1].min)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "does not silently sanitize invalid UTF-8 cells while clipping" do
|
|
143
|
+
@db.execute("CREATE TABLE cells (value BLOB)")
|
|
144
|
+
@db.execute("INSERT INTO cells VALUES (?)", [SQLite3::Blob.new("\xFF".b + "a" * 70_000)])
|
|
145
|
+
expect do
|
|
146
|
+
@corpus.send(:run_sql_file_on, @db, "SELECT value FROM cells", File.join(@dir, "invalid.jsonl"))
|
|
147
|
+
end.to raise_error(JSON::GeneratorError)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "clips by bytes including the ellipsis without splitting UTF-8" do
|
|
151
|
+
@db.execute("CREATE TABLE cells (value TEXT)")
|
|
152
|
+
values = ["あ" * 30_000, "😀" * 20_000, "é" * 40_000, "", "a" * 65_536]
|
|
153
|
+
values.each { |value| @db.execute("INSERT INTO cells VALUES (?)", [value]) }
|
|
154
|
+
path = File.join(@dir, "cells.jsonl")
|
|
155
|
+
result = @corpus.send(:run_sql_file_on, @db, "SELECT value FROM cells", path)
|
|
156
|
+
output = File.readlines(path).map { |line| JSON.parse(line)["value"] }
|
|
157
|
+
expect(result[:cells_clipped]).to eq(3)
|
|
158
|
+
expect(output.all?(&:valid_encoding?)).to be true
|
|
159
|
+
expect(output.map(&:bytesize).max).to be <= 65_536
|
|
160
|
+
expect(output.first(3).all? { |value| value.end_with?("…") }).to be true
|
|
161
|
+
expect(output.last(2)).to eq(values.last(2))
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe "exclusive staged output" do
|
|
166
|
+
let(:out) { File.join(@dir, "成果.jsonl") }
|
|
167
|
+
|
|
168
|
+
def publish(path, overwrite: false)
|
|
169
|
+
Wp2txt::OutputPath.write_pair(path, overwrite: overwrite) do |data, meta|
|
|
170
|
+
File.write(data, "本文")
|
|
171
|
+
File.write(meta, "{}")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "rejects symlink directories, output files, dangling links, and sidecars even with overwrite" do
|
|
176
|
+
target = File.join(@dir, "target")
|
|
177
|
+
Dir.mkdir(target)
|
|
178
|
+
link = File.join(@dir, "link")
|
|
179
|
+
File.symlink(target, link)
|
|
180
|
+
expect { publish(File.join(link, "out"), overwrite: true) }.to raise_error(ArgumentError, /symbolic/)
|
|
181
|
+
[out, "#{out}.meta.json"].each do |destination|
|
|
182
|
+
File.symlink(File.join(target, "missing"), destination)
|
|
183
|
+
expect { publish(out, overwrite: true) }.to raise_error(ArgumentError, /symbolic/)
|
|
184
|
+
expect { Wp2txt::OutputPath.confine(out, @dir, overwrite: true) }.to raise_error(ArgumentError, /symbolic/)
|
|
185
|
+
File.unlink(destination)
|
|
186
|
+
end
|
|
187
|
+
expect(Dir.children(target)).to be_empty
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "rejects an existing sidecar without creating the output" do
|
|
191
|
+
File.write("#{out}.meta.json", "old")
|
|
192
|
+
expect { publish(out) }.to raise_error(ArgumentError, /already exists/)
|
|
193
|
+
expect(File.exist?(out)).to be false
|
|
194
|
+
expect(File.read("#{out}.meta.json")).to eq("old")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "uses EXCL even if a file arrives after validation" do
|
|
198
|
+
allow(Wp2txt::OutputPath).to receive(:validate_pair!).and_wrap_original do |method, *args, **kwargs|
|
|
199
|
+
method.call(*args, **kwargs)
|
|
200
|
+
File.write(out, "competing writer")
|
|
201
|
+
end
|
|
202
|
+
expect { publish(out) }.to raise_error(ArgumentError, /already exists/)
|
|
203
|
+
expect(File.read(out)).to eq("competing writer")
|
|
204
|
+
expect(File.exist?("#{out}.meta.json")).to be false
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "cleans its own reservation when sidecar reservation loses a race" do
|
|
208
|
+
allow(Wp2txt::OutputPath).to receive(:validate_pair!).and_wrap_original do |method, *args, **kwargs|
|
|
209
|
+
method.call(*args, **kwargs)
|
|
210
|
+
File.write("#{out}.meta.json", "competing writer")
|
|
211
|
+
end
|
|
212
|
+
expect { publish(out) }.to raise_error(ArgumentError, /already exists/)
|
|
213
|
+
expect(File.exist?(out)).to be false
|
|
214
|
+
expect(File.read("#{out}.meta.json")).to eq("competing writer")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "publishes both files and leaves no temporary files" do
|
|
218
|
+
publish(out)
|
|
219
|
+
expect(File.read(out)).to eq("本文")
|
|
220
|
+
expect(File.read("#{out}.meta.json")).to eq("{}")
|
|
221
|
+
expect(Dir.children(@dir).sort).to eq([File.basename(out), File.basename(out) + ".meta.json"].sort)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it "preserves old files if staging fails during an overwrite" do
|
|
225
|
+
File.write(out, "old data")
|
|
226
|
+
File.write("#{out}.meta.json", "old meta")
|
|
227
|
+
expect do
|
|
228
|
+
Wp2txt::OutputPath.write_pair(out, overwrite: true) do |data, _meta|
|
|
229
|
+
File.write(data, "incomplete")
|
|
230
|
+
raise "failed"
|
|
231
|
+
end
|
|
232
|
+
end.to raise_error("failed")
|
|
233
|
+
expect(File.read(out)).to eq("old data")
|
|
234
|
+
expect(File.read("#{out}.meta.json")).to eq("old meta")
|
|
235
|
+
expect(Dir.glob(File.join(@dir, ".wp2txt-*"))).to be_empty
|
|
236
|
+
publish(out, overwrite: true)
|
|
237
|
+
expect(File.read(out)).to eq("本文")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it "allows only one concurrent writer without overwrite" do
|
|
241
|
+
entered = Queue.new
|
|
242
|
+
release = Queue.new
|
|
243
|
+
writer = Thread.new do
|
|
244
|
+
Wp2txt::OutputPath.write_pair(out) do |data, meta|
|
|
245
|
+
entered << true
|
|
246
|
+
release.pop
|
|
247
|
+
File.write(data, "first")
|
|
248
|
+
File.write(meta, "{}")
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
Timeout.timeout(3) { entered.pop }
|
|
252
|
+
expect { publish(out) }.to raise_error(ArgumentError, /already exists/)
|
|
253
|
+
release << true
|
|
254
|
+
writer.value
|
|
255
|
+
expect(File.read(out)).to eq("first")
|
|
256
|
+
ensure
|
|
257
|
+
release << true
|
|
258
|
+
writer&.join
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
describe "Parallel finish contract" do
|
|
263
|
+
[0, 2].each do |workers|
|
|
264
|
+
it "delivers each payload to finish but retains none with #{workers} workers" do
|
|
265
|
+
skip "fork unavailable" if workers.positive? && !Process.respond_to?(:fork)
|
|
266
|
+
payloads = ["日本語", "\xFF".b, ""]
|
|
267
|
+
received = []
|
|
268
|
+
returned = Parallel.each(payloads, in_processes: workers,
|
|
269
|
+
finish: ->(_item, _i, value) { received << value }) { |value| "rendered:#{value.bytesize}" }
|
|
270
|
+
expect(received).to match_array(payloads.map { |value| "rendered:#{value.bytesize}" })
|
|
271
|
+
expect(Array(returned).flatten.grep(/\Arendered:/)).to be_empty
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it "builds both indexes while disabling retained batch results" do
|
|
276
|
+
dump, index_path = create_fixture(@dir)
|
|
277
|
+
index = Wp2txt::MultistreamIndex.new(index_path, use_cache: false, show_progress: false)
|
|
278
|
+
calls = []
|
|
279
|
+
allow(Parallel).to receive(:each).and_wrap_original do |method, source, options, &block|
|
|
280
|
+
calls << options
|
|
281
|
+
method.call(source, options, &block)
|
|
282
|
+
end
|
|
283
|
+
meta_path = Wp2txt::MetadataIndex.path_for(dump, cache_dir: @dir)
|
|
284
|
+
meta = Wp2txt::MetadataIndexBuilder.new(dump, index.stream_offsets, db_path: meta_path, num_processes: 0).build
|
|
285
|
+
expect(meta.stats[:article_count]).to eq(3)
|
|
286
|
+
meta.close
|
|
287
|
+
fts = Wp2txt::FtsIndexBuilder.new(dump, index.stream_offsets,
|
|
288
|
+
db_path: File.join(@dir, "fts.sqlite3"), meta_db_path: meta_path, num_processes: 0).build
|
|
289
|
+
expect(fts.search("Story", count: "exact")[:total]).to eq(2)
|
|
290
|
+
expect(calls.size).to eq(2)
|
|
291
|
+
expect(calls.all? { |options| options[:finish] && !options.key?(:preserve_results) }).to be true
|
|
292
|
+
fts.close
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
describe "job lifecycle" do
|
|
297
|
+
it "records factory failure and permits a subsequent job" do
|
|
298
|
+
manager = Wp2txt::CorpusJobManager.new(-> { raise "factory failure" })
|
|
299
|
+
first = manager.start_extract({})
|
|
300
|
+
Timeout.timeout(3) { Thread.pass until manager.status(first[:job_id])[:status] == "error" }
|
|
301
|
+
expect(manager.status(first[:job_id])[:error]).to include("factory failure")
|
|
302
|
+
second = manager.start_extract({})
|
|
303
|
+
expect(second[:job_id]).not_to eq(first[:job_id])
|
|
304
|
+
Timeout.timeout(3) { Thread.pass until manager.status(second[:job_id])[:status] == "error" }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it "atomically admits one job from simultaneous callers" do
|
|
308
|
+
release = Queue.new
|
|
309
|
+
fake = Object.new
|
|
310
|
+
fake.define_singleton_method(:extract_corpus) { |**_params| release.pop; {} }
|
|
311
|
+
fake.define_singleton_method(:close) {}
|
|
312
|
+
manager = Wp2txt::CorpusJobManager.new(-> { fake })
|
|
313
|
+
# Yield during state construction, precisely where the old implementation
|
|
314
|
+
# had released its check lock but had not registered the running job.
|
|
315
|
+
allow(Time).to receive(:now).and_wrap_original do |method|
|
|
316
|
+
sleep 0.005
|
|
317
|
+
method.call
|
|
318
|
+
end
|
|
319
|
+
gate = Queue.new
|
|
320
|
+
callers = Array.new(12) { Thread.new { gate.pop; manager.start_extract({}) } }
|
|
321
|
+
12.times { gate << true }
|
|
322
|
+
results = callers.map(&:value)
|
|
323
|
+
expect(results.count { |result| result[:job_id] }).to eq(1)
|
|
324
|
+
expect(results.count { |result| result[:error] }).to eq(11)
|
|
325
|
+
release << true
|
|
326
|
+
Timeout.timeout(3) { Thread.pass until manager.list.first[:status] == "completed" }
|
|
327
|
+
ensure
|
|
328
|
+
release << true
|
|
329
|
+
callers&.each(&:join)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
describe "image verification" do
|
|
334
|
+
around do |example|
|
|
335
|
+
previous = Rake.application
|
|
336
|
+
Rake.application = Rake::Application.new
|
|
337
|
+
Rake::Task.define_task(:build)
|
|
338
|
+
suppress_stderr { load File.expand_path("../Rakefile", __dir__) }
|
|
339
|
+
example.run
|
|
340
|
+
ensure
|
|
341
|
+
Rake.application = previous
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
[[false, "Cannot connect to daemon"], [false, "image missing"], [true, "LEAK:/wp2txt/tmp\n"]].each do |success, output|
|
|
345
|
+
it "fails verification for #{output.strip}" do
|
|
346
|
+
allow(Open3).to receive(:capture2e).and_return([output, double(success?: success)])
|
|
347
|
+
expect { suppress_stderr { Rake::Task[:verify_image].invoke("test-image") } }.to raise_error(SystemExit)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it "passes only after a successful clean container inspection" do
|
|
352
|
+
expect(Open3).to receive(:capture2e).with("docker", "run", "--rm", "test-image", "sh", "-c", kind_of(String))
|
|
353
|
+
.and_return(["", double(success?: true)])
|
|
354
|
+
expect { Rake::Task[:verify_image].invoke("test-image") }.to output(/OK:/).to_stdout
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
@@ -112,12 +112,14 @@ RSpec.describe Wp2txt::StreamProcessor do
|
|
|
112
112
|
</page>
|
|
113
113
|
<page>
|
|
114
114
|
<title>Wikipedia:Help</title>
|
|
115
|
+
<ns>4</ns>
|
|
115
116
|
<revision>
|
|
116
117
|
<text>Help content.</text>
|
|
117
118
|
</revision>
|
|
118
119
|
</page>
|
|
119
120
|
<page>
|
|
120
121
|
<title>File:Image.jpg</title>
|
|
122
|
+
<ns>6</ns>
|
|
121
123
|
<revision>
|
|
122
124
|
<text>File description.</text>
|
|
123
125
|
</revision>
|
|
@@ -131,7 +133,7 @@ RSpec.describe Wp2txt::StreamProcessor do
|
|
|
131
133
|
File.write(xml_file, xml_content)
|
|
132
134
|
end
|
|
133
135
|
|
|
134
|
-
it "skips
|
|
136
|
+
it "skips non-article namespaces" do
|
|
135
137
|
processor = described_class.new(xml_file)
|
|
136
138
|
pages = processor.each_page.to_a
|
|
137
139
|
|
|
@@ -237,6 +237,46 @@ RSpec.describe "titles extraction and SQL file output" do
|
|
|
237
237
|
expect(File.exist?("#{out}.partial")).to be false
|
|
238
238
|
end
|
|
239
239
|
|
|
240
|
+
it "records ordinal to original name mappings in the sidecar for colliding names" do
|
|
241
|
+
out = File.join(@dir, "names.jsonl")
|
|
242
|
+
@corpus.query_sql('SELECT 1 AS x, 2 AS x, 3 AS x_2', output_path: out)
|
|
243
|
+
expect(read_jsonl(out)).to eq([{ "x" => 1, "x_3" => 2, "x_2" => 3 }])
|
|
244
|
+
meta = JSON.parse(File.read("#{out}.meta.json"))
|
|
245
|
+
expect(meta["column_mapping"]).to eq([
|
|
246
|
+
{ "ordinal" => 0, "original_name" => "x", "output_name" => "x" },
|
|
247
|
+
{ "ordinal" => 1, "original_name" => "x", "output_name" => "x_3" },
|
|
248
|
+
{ "ordinal" => 2, "original_name" => "x_2", "output_name" => "x_2" }
|
|
249
|
+
])
|
|
250
|
+
expect(meta["cell_byte_limit"]).to eq(65_536)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "refuses sidecar collisions through both public output APIs" do
|
|
254
|
+
out = File.join(@dir, "sidecar.jsonl")
|
|
255
|
+
File.write("#{out}.meta.json", "old")
|
|
256
|
+
expect { @corpus.query_sql("SELECT 1", output_path: out) }.to raise_error(ArgumentError, /already exists/)
|
|
257
|
+
expect do
|
|
258
|
+
@corpus.extract_corpus(output_path: out, content: "summary", titles: ["Film A"], num_processes: 0)
|
|
259
|
+
end.to raise_error(ArgumentError, /already exists/)
|
|
260
|
+
expect(File.exist?(out)).to be false
|
|
261
|
+
expect(File.read("#{out}.meta.json")).to eq("old")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it "supports explicit overwrite for extraction and cleans up cancelled staging" do
|
|
265
|
+
out = File.join(@dir, "extract.jsonl")
|
|
266
|
+
File.write(out, "old")
|
|
267
|
+
File.write("#{out}.meta.json", "old metadata")
|
|
268
|
+
expect do
|
|
269
|
+
@corpus.extract_corpus(output_path: out, content: "summary", titles: ["Film A"],
|
|
270
|
+
num_processes: 0, overwrite: true, cancel_check: -> { true })
|
|
271
|
+
end.to raise_error(Wp2txt::Corpus::Cancelled)
|
|
272
|
+
expect(File.read(out)).to eq("old")
|
|
273
|
+
expect(File.read("#{out}.meta.json")).to eq("old metadata")
|
|
274
|
+
expect(Dir.glob(File.join(@dir, ".wp2txt-*"))).to be_empty
|
|
275
|
+
@corpus.extract_corpus(output_path: out, content: "summary", titles: ["Film A"],
|
|
276
|
+
num_processes: 0, overwrite: true)
|
|
277
|
+
expect(read_jsonl(out).first["title"]).to eq("Film A")
|
|
278
|
+
end
|
|
279
|
+
|
|
240
280
|
it "ignores limit in file-output mode" do
|
|
241
281
|
out = File.join(@dir, "q.jsonl")
|
|
242
282
|
result = @corpus.query_sql("SELECT title FROM pages", output_path: out, limit: 1)
|
|
@@ -255,6 +295,14 @@ RSpec.describe "titles extraction and SQL file output" do
|
|
|
255
295
|
expect(read_jsonl(out)).to eq([{ "one" => 1 }])
|
|
256
296
|
end
|
|
257
297
|
|
|
298
|
+
it "does not touch another writer's fixed .partial path" do
|
|
299
|
+
out = File.join(@dir, "q.jsonl")
|
|
300
|
+
File.write("#{out}.partial", "another writer")
|
|
301
|
+
@corpus.query_sql("SELECT 1", output_path: out)
|
|
302
|
+
expect(File.read("#{out}.partial")).to eq("another writer")
|
|
303
|
+
expect(Dir.glob(File.join(@dir, ".wp2txt-*"))).to be_empty
|
|
304
|
+
end
|
|
305
|
+
|
|
258
306
|
it "removes .partial and leaves no output on SQL error" do
|
|
259
307
|
out = File.join(@dir, "q.jsonl")
|
|
260
308
|
expect { @corpus.query_sql("SELECT * FROM no_such_table", output_path: out) }
|
|
@@ -291,7 +339,7 @@ RSpec.describe "titles extraction and SQL file output" do
|
|
|
291
339
|
|
|
292
340
|
expect(result[:cells_clipped]).to eq(1)
|
|
293
341
|
value = read_jsonl(out).first["big"]
|
|
294
|
-
expect(value.
|
|
342
|
+
expect(value.bytesize).to eq(Wp2txt::Corpus::SQL_FILE_CELL_LIMIT)
|
|
295
343
|
end
|
|
296
344
|
|
|
297
345
|
it "writes a .meta.json sidecar with SQL, attach provenance, and counts" do
|
data/wp2txt.gemspec
CHANGED
|
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
|
|
|
13
13
|
s.license = "MIT"
|
|
14
14
|
s.required_ruby_version = Gem::Requirement.new(">= 3.0")
|
|
15
15
|
s.files = `git ls-files`.split("\n")
|
|
16
|
-
|
|
16
|
+
.reject { |f| f.start_with?("data/", "image/", "scripts/") }
|
|
17
17
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
18
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
|
19
19
|
s.require_paths = ["lib"]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wp2txt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.3.
|
|
4
|
+
version: 2.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yoichiro Hasebe
|
|
@@ -204,6 +204,7 @@ files:
|
|
|
204
204
|
- ".dockerignore"
|
|
205
205
|
- ".github/FUNDING.yml"
|
|
206
206
|
- ".github/workflows/ci.yml"
|
|
207
|
+
- ".github/workflows/publish-image.yml"
|
|
207
208
|
- ".gitignore"
|
|
208
209
|
- ".solargraph.yml"
|
|
209
210
|
- CHANGELOG.md
|
|
@@ -218,8 +219,6 @@ files:
|
|
|
218
219
|
- bin/wp2txt
|
|
219
220
|
- bin/wp2txt-mcp
|
|
220
221
|
- docs/INDEXES.md
|
|
221
|
-
- image/wp2txt-logo.svg
|
|
222
|
-
- image/wp2txt.svg
|
|
223
222
|
- lib/wp2txt.rb
|
|
224
223
|
- lib/wp2txt/article.rb
|
|
225
224
|
- lib/wp2txt/bz2_validator.rb
|
|
@@ -259,12 +258,6 @@ files:
|
|
|
259
258
|
- lib/wp2txt/text_processing.rb
|
|
260
259
|
- lib/wp2txt/utils.rb
|
|
261
260
|
- lib/wp2txt/version.rb
|
|
262
|
-
- scripts/benchmark_regex.rb
|
|
263
|
-
- scripts/fetch_html_entities.rb
|
|
264
|
-
- scripts/fetch_language_metadata.rb
|
|
265
|
-
- scripts/fetch_mediawiki_data.rb
|
|
266
|
-
- scripts/fetch_template_data.rb
|
|
267
|
-
- scripts/profile_memory.rb
|
|
268
261
|
- spec/article_spec.rb
|
|
269
262
|
- spec/auto_download_spec.rb
|
|
270
263
|
- spec/bz2_validator_spec.rb
|
|
@@ -292,6 +285,7 @@ files:
|
|
|
292
285
|
- spec/multi_dump_attach_spec.rb
|
|
293
286
|
- spec/multistream_spec.rb
|
|
294
287
|
- spec/output_writer_spec.rb
|
|
288
|
+
- spec/p1_correctness_spec.rb
|
|
295
289
|
- spec/parser_functions_spec.rb
|
|
296
290
|
- spec/ractor_worker_spec.rb
|
|
297
291
|
- spec/regex_spec.rb
|
|
@@ -358,6 +352,7 @@ test_files:
|
|
|
358
352
|
- spec/multi_dump_attach_spec.rb
|
|
359
353
|
- spec/multistream_spec.rb
|
|
360
354
|
- spec/output_writer_spec.rb
|
|
355
|
+
- spec/p1_correctness_spec.rb
|
|
361
356
|
- spec/parser_functions_spec.rb
|
|
362
357
|
- spec/ractor_worker_spec.rb
|
|
363
358
|
- spec/regex_spec.rb
|
data/image/wp2txt-logo.svg
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<svg viewBox="46.275 153.235 274.248 70.635" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
-
<g style="" transform="matrix(0.140807, 0, 0, 0.140807, 46.270237, 160.188004)">
|
|
4
|
-
<path style="fill:#194F82;" d="M359.854,0H32.743C14.707,0,0.032,14.675,0.032,32.711v241.972 c0,18.036,14.675,32.711,32.711,32.711h91.798l-5.495,21.786H91.83c-6.012,0-10.925,4.848-10.925,10.925v41.503 c0,6.012,4.848,10.925,10.925,10.925h208.873c6.012,0,10.925-4.848,10.925-10.925v-41.503c0-6.012-4.848-10.925-10.925-10.925 h-27.216l-5.495-21.786h91.798c18.036,0,32.711-14.675,32.711-32.711V32.711C392.566,14.675,377.891,0,359.854,0z M289.778,351.03 v19.717H102.82V351.03h24.824c5.042,0,9.374-3.426,10.602-8.275l8.792-35.362h98.586l8.792,35.362 c1.228,4.848,5.56,8.275,10.602,8.275H289.778z M359.854,285.608H32.743c-6.012,0-10.925-4.848-10.925-10.925V32.711 c0-6.012,4.848-10.925,10.925-10.925h327.111c6.012,0,10.925,4.848,10.925,10.925v242.036h0.065 C370.78,280.695,365.931,285.608,359.854,285.608z"/>
|
|
5
|
-
<path style="fill:#194F82;" d="M338.004,43.572H54.594c-6.012,0-10.925,4.848-10.925,10.925v198.335 c0,6.012,4.848,10.925,10.925,10.925h283.539c6.012,0,10.925-4.848,10.925-10.925V54.562 C348.994,48.549,344.016,43.572,338.004,43.572z M327.208,241.907h-0.065H65.454v-88.566h13.188 c6.012,0,10.925-4.848,10.925-10.925c0-6.012-4.848-10.925-10.925-10.925H65.454V109.64h34.974c6.012,0,10.925-4.848,10.925-10.925 c0-6.012-4.848-10.925-10.925-10.925H65.454V65.293h261.754V241.907L327.208,241.907z"/>
|
|
6
|
-
<path style="fill:#194F82;" d="M109.155,161.099l34.521,34.521c6.723,6.206,13.317,2.133,15.451,0 c4.267-4.267,4.267-11.119,0-15.451l-26.828-26.764l26.828-26.828c4.267-4.267,4.267-11.119,0-15.451 c-4.267-4.267-11.119-4.267-15.451,0l-34.521,34.521C104.889,149.915,104.889,156.768,109.155,161.099z"/>
|
|
7
|
-
<path style="fill:#194F82;" d="M233.535,195.62c4.978,5.042,12.283,3.556,15.451,0l34.521-34.521 c4.267-4.267,4.267-11.119,0-15.451l-34.521-34.521c-4.267-4.267-11.119-4.267-15.451,0c-4.267,4.267-4.267,11.119,0,15.451 l26.828,26.828l-26.828,26.828C229.269,184.436,229.269,191.289,233.535,195.62z"/>
|
|
8
|
-
<path style="fill:#194F82;" d="M174.254,219.152c6.853,2.069,12.283-2.263,13.834-6.788l37.883-110.222 c1.939-5.689-1.099-11.895-6.788-13.834c-5.689-1.939-11.895,1.099-13.834,6.788l-37.883,110.222 C165.527,210.941,168.501,217.341,174.254,219.152z"/>
|
|
9
|
-
</g>
|
|
10
|
-
<path d="M 187.019 174.178 L 187.537 174.178 Q 188.054 174.178 188.266 174.484 Q 188.477 174.79 188.477 175.001 Q 188.477 175.213 188.43 175.354 L 180.012 205.593 Q 179.542 207.145 178.061 207.145 Q 176.579 207.145 176.156 205.593 L 170.371 184.901 L 164.587 205.687 Q 164.07 207.145 162.682 207.145 Q 161.295 207.145 160.778 205.687 L 151.89 175.401 Q 151.842 175.213 151.842 174.978 Q 151.842 174.743 152.054 174.461 Q 152.266 174.178 152.783 174.178 L 153.253 174.178 Q 153.818 174.178 154.147 174.837 L 162.706 203.9 L 168.773 182.22 Q 169.149 180.903 170.442 180.903 Q 171.735 180.903 172.017 182.267 Q 173.005 185.841 175.051 193.06 Q 177.096 200.279 178.084 203.853 L 186.126 174.884 Q 186.22 174.649 186.479 174.414 Q 186.737 174.178 187.019 174.178 Z" transform="matrix(1.352871, 0, 0, 1.231972, -93.713486, -46.78933)" style="fill: rgb(25, 79, 130); line-height: 73.9473px; white-space: pre;"/>
|
|
11
|
-
<path d="M 193.83 174.178 L 201.12 174.178 Q 204.412 174.178 207.186 175.966 Q 208.644 176.906 209.538 178.693 Q 210.431 180.48 210.431 182.926 Q 210.431 186.641 208.268 189.157 Q 206.105 191.673 200.743 191.673 L 197.64 191.673 Q 197.216 191.673 196.958 191.391 Q 196.699 191.109 196.699 190.732 L 196.699 190.309 Q 196.699 189.369 197.64 189.369 L 200.179 189.369 Q 205.305 189.369 206.81 187.205 Q 208.08 185.418 208.08 182.879 Q 208.08 179.54 205.94 178.035 Q 203.8 176.53 200.884 176.53 L 194.112 176.53 L 194.112 206.157 Q 194.112 206.534 193.83 206.816 Q 193.548 207.098 193.172 207.098 L 192.702 207.098 Q 192.325 207.098 192.043 206.816 Q 191.761 206.534 191.761 206.157 L 191.761 176.248 Q 191.761 175.025 192.184 174.602 Q 192.608 174.178 193.83 174.178 Z" transform="matrix(1.352871, 0, 0, 1.231972, -93.713486, -46.78933)" style="fill: rgb(25, 79, 130); line-height: 73.9473px; white-space: pre;"/>
|
|
12
|
-
<path d="M 224.167 174.178 Q 228.258 174.178 230.468 176.318 Q 232.679 178.458 232.679 181.985 Q 232.679 183.96 231.456 186.523 Q 230.233 189.086 229.057 190.732 Q 226.33 194.636 221.721 199.95 L 220.31 201.643 Q 219.135 203.054 218.5 203.9 Q 217.865 204.747 217.724 204.888 Q 217.818 204.841 218.006 204.841 L 232.443 204.841 Q 233.384 204.841 233.384 205.734 L 233.384 206.157 Q 233.384 207.098 232.443 207.098 L 217.959 207.098 Q 217.206 207.098 216.76 207.004 Q 216.313 206.91 215.866 206.369 Q 215.419 205.828 215.419 204.864 Q 215.419 203.9 217.442 201.361 L 219.417 199.103 Q 219.84 198.586 220.357 197.975 Q 221.58 196.564 222.568 195.294 L 223.837 193.789 Q 224.119 193.413 224.99 192.308 Q 225.86 191.203 226.189 190.779 Q 226.518 190.356 227.247 189.369 Q 227.976 188.381 228.281 187.84 Q 228.587 187.299 229.081 186.429 Q 229.575 185.559 229.81 184.948 Q 230.374 183.49 230.374 182.22 Q 230.374 179.258 228.54 177.847 Q 226.706 176.436 224.167 176.436 Q 219.652 176.436 217.912 180.668 Q 217.63 181.233 217.065 181.233 L 216.642 181.233 Q 215.701 181.233 215.701 180.386 Q 215.701 180.151 215.749 180.057 Q 216.689 177.282 218.97 175.73 Q 221.251 174.178 224.167 174.178 Z" transform="matrix(1.352871, 0, 0, 1.231972, -93.713486, -46.78933)" style="fill: rgb(25, 79, 130); line-height: 73.9473px; white-space: pre;"/>
|
|
13
|
-
<path d="M 247.486 177.753 L 247.956 177.753 Q 248.897 177.753 248.897 178.646 L 248.897 206.157 Q 248.897 206.534 248.638 206.816 Q 248.38 207.098 247.956 207.098 L 247.486 207.098 Q 247.063 207.098 246.804 206.816 Q 246.545 206.534 246.545 206.157 L 246.545 178.646 Q 246.545 177.753 247.486 177.753 Z M 237.328 174.178 L 258.208 174.178 Q 258.632 174.178 258.89 174.461 Q 259.149 174.743 259.149 175.119 L 259.149 175.636 Q 259.149 176.483 258.208 176.483 L 237.328 176.483 Q 236.387 176.483 236.387 175.636 L 236.387 175.119 Q 236.387 174.743 236.67 174.461 Q 236.952 174.178 237.328 174.178 Z" transform="matrix(1.352871, 0, 0, 1.231972, -93.713486, -46.78933)" style="fill: rgb(25, 79, 130); line-height: 73.9473px; white-space: pre;"/>
|
|
14
|
-
<path d="M 279.324 174.178 L 279.841 174.178 Q 280.405 174.178 280.664 174.649 Q 280.923 175.119 280.64 175.589 L 280.64 175.542 L 272.693 190.027 Q 272.646 190.074 272.528 190.262 Q 272.411 190.45 272.317 190.544 L 272.599 190.92 L 281.111 205.734 Q 281.252 205.969 281.252 206.204 Q 281.252 206.44 281.064 206.769 Q 280.876 207.098 280.311 207.098 L 279.747 207.098 Q 279.23 207.098 279.042 206.816 L 270.906 192.707 Q 270.435 193.507 267.167 198.751 Q 263.899 203.994 262.253 206.675 Q 261.923 207.098 261.453 207.098 L 260.889 207.098 Q 260.277 207.098 260.042 206.628 Q 259.807 206.157 260.089 205.64 L 269.307 190.92 L 269.307 190.967 Q 269.495 190.591 269.636 190.45 Q 269.448 190.262 269.401 190.121 L 259.666 175.636 Q 259.525 175.401 259.525 175.166 Q 259.525 174.931 259.713 174.555 Q 259.901 174.178 260.465 174.178 L 260.983 174.178 Q 261.453 174.178 261.782 174.602 L 271 188.287 L 278.571 174.461 Q 278.759 174.178 279.324 174.178 Z" transform="matrix(1.352871, 0, 0, 1.231972, -93.713486, -46.78933)" style="fill: rgb(25, 79, 130); line-height: 73.9473px; white-space: pre;"/>
|
|
15
|
-
<path d="M 293.994 177.753 L 294.464 177.753 Q 295.405 177.753 295.405 178.646 L 295.405 206.157 Q 295.405 206.534 295.146 206.816 Q 294.888 207.098 294.464 207.098 L 293.994 207.098 Q 293.571 207.098 293.312 206.816 Q 293.054 206.534 293.054 206.157 L 293.054 178.646 Q 293.054 177.753 293.994 177.753 Z M 283.836 174.178 L 304.716 174.178 Q 305.14 174.178 305.398 174.461 Q 305.657 174.743 305.657 175.119 L 305.657 175.636 Q 305.657 176.483 304.716 176.483 L 283.836 176.483 Q 282.896 176.483 282.896 175.636 L 282.896 175.119 Q 282.896 174.743 283.178 174.461 Q 283.46 174.178 283.836 174.178 Z" transform="matrix(1.352871, 0, 0, 1.231972, -93.713486, -46.78933)" style="fill: rgb(25, 79, 130); line-height: 73.9473px; white-space: pre;"/>
|
|
16
|
-
</svg>
|
data/image/wp2txt.svg
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<svg version="1.1" id="Layer_1" x="0px" y="0px" style="enable-background:new 0 0 392.533 392.533;" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
|
|
3
|
-
<defs>
|
|
4
|
-
<bx:guide x="212.88" y="167.188" angle="90"/>
|
|
5
|
-
<bx:guide x="327.22" y="208.912" angle="90"/>
|
|
6
|
-
<style bx:fonts="Text Me One">@import url(https://fonts.googleapis.com/css2?family=Text+Me+One%3Aital%2Cwght%400%2C400&display=swap);</style>
|
|
7
|
-
</defs>
|
|
8
|
-
<g style="" transform="matrix(0.140807, 0, 0, 0.140807, 46.270237, 160.188004)">
|
|
9
|
-
<path style="fill:#194F82;" d="M359.854,0H32.743C14.707,0,0.032,14.675,0.032,32.711v241.972 c0,18.036,14.675,32.711,32.711,32.711h91.798l-5.495,21.786H91.83c-6.012,0-10.925,4.848-10.925,10.925v41.503 c0,6.012,4.848,10.925,10.925,10.925h208.873c6.012,0,10.925-4.848,10.925-10.925v-41.503c0-6.012-4.848-10.925-10.925-10.925 h-27.216l-5.495-21.786h91.798c18.036,0,32.711-14.675,32.711-32.711V32.711C392.566,14.675,377.891,0,359.854,0z M289.778,351.03 v19.717H102.82V351.03h24.824c5.042,0,9.374-3.426,10.602-8.275l8.792-35.362h98.586l8.792,35.362 c1.228,4.848,5.56,8.275,10.602,8.275H289.778z M359.854,285.608H32.743c-6.012,0-10.925-4.848-10.925-10.925V32.711 c0-6.012,4.848-10.925,10.925-10.925h327.111c6.012,0,10.925,4.848,10.925,10.925v242.036h0.065 C370.78,280.695,365.931,285.608,359.854,285.608z"/>
|
|
10
|
-
<path style="fill:#194F82;" d="M338.004,43.572H54.594c-6.012,0-10.925,4.848-10.925,10.925v198.335 c0,6.012,4.848,10.925,10.925,10.925h283.539c6.012,0,10.925-4.848,10.925-10.925V54.562 C348.994,48.549,344.016,43.572,338.004,43.572z M327.208,241.907h-0.065H65.454v-88.566h13.188 c6.012,0,10.925-4.848,10.925-10.925c0-6.012-4.848-10.925-10.925-10.925H65.454V109.64h34.974c6.012,0,10.925-4.848,10.925-10.925 c0-6.012-4.848-10.925-10.925-10.925H65.454V65.293h261.754V241.907L327.208,241.907z"/>
|
|
11
|
-
<path style="fill:#194F82;" d="M109.155,161.099l34.521,34.521c6.723,6.206,13.317,2.133,15.451,0 c4.267-4.267,4.267-11.119,0-15.451l-26.828-26.764l26.828-26.828c4.267-4.267,4.267-11.119,0-15.451 c-4.267-4.267-11.119-4.267-15.451,0l-34.521,34.521C104.889,149.915,104.889,156.768,109.155,161.099z"/>
|
|
12
|
-
<path style="fill:#194F82;" d="M233.535,195.62c4.978,5.042,12.283,3.556,15.451,0l34.521-34.521 c4.267-4.267,4.267-11.119,0-15.451l-34.521-34.521c-4.267-4.267-11.119-4.267-15.451,0c-4.267,4.267-4.267,11.119,0,15.451 l26.828,26.828l-26.828,26.828C229.269,184.436,229.269,191.289,233.535,195.62z"/>
|
|
13
|
-
<path style="fill:#194F82;" d="M174.254,219.152c6.853,2.069,12.283-2.263,13.834-6.788l37.883-110.222 c1.939-5.689-1.099-11.895-6.788-13.834c-5.689-1.939-11.895,1.099-13.834,6.788l-37.883,110.222 C165.527,210.941,168.501,217.341,174.254,219.152z"/>
|
|
14
|
-
</g>
|
|
15
|
-
<g/>
|
|
16
|
-
<g/>
|
|
17
|
-
<g/>
|
|
18
|
-
<g/>
|
|
19
|
-
<g/>
|
|
20
|
-
<g/>
|
|
21
|
-
<g/>
|
|
22
|
-
<g/>
|
|
23
|
-
<g/>
|
|
24
|
-
<g/>
|
|
25
|
-
<g/>
|
|
26
|
-
<g/>
|
|
27
|
-
<g/>
|
|
28
|
-
<g/>
|
|
29
|
-
<g/>
|
|
30
|
-
<text style="fill: rgb(25, 79, 130); font-family: "Text Me One"; font-size: 47.0279px; line-height: 73.9473px; white-space: pre;" transform="matrix(1.352871, 0, 0, 1.231972, -93.713486, -46.78933)" x="152.689" y="207.098">WP2TXT</text>
|
|
31
|
-
</svg>
|