wp2txt 2.2.0 → 2.3.1

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.
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tmpdir"
5
+ require "json"
6
+ require_relative "support/multistream_fixture"
7
+ require_relative "support/meta_db_fixture"
8
+ require_relative "../lib/wp2txt/corpus"
9
+ require_relative "../lib/wp2txt/corpus_jobs"
10
+ require_relative "../lib/wp2txt/output_path"
11
+
12
+ # extract_corpus titles: and query_sql output_path: (design doc 04)
13
+ RSpec.describe "titles extraction and SQL file output" do
14
+ include MultistreamFixture
15
+ include MetaDbFixture
16
+
17
+ around do |example|
18
+ Dir.mktmpdir do |dir|
19
+ @dir = dir
20
+ @multistream_path, @index_path = create_fixture(dir)
21
+
22
+ ms_index = Wp2txt::MultistreamIndex.new(@index_path, use_cache: false, show_progress: false)
23
+ @db_path = Wp2txt::MetadataIndex.path_for(@multistream_path, cache_dir: dir)
24
+ Wp2txt::MetadataIndexBuilder.new(
25
+ @multistream_path, ms_index.stream_offsets,
26
+ db_path: @db_path, num_processes: 0
27
+ ).build
28
+
29
+ @corpus = Wp2txt::Corpus.for_input(@multistream_path, cache_dir: dir)
30
+ example.run
31
+ @corpus.close
32
+ end
33
+ end
34
+
35
+ def read_jsonl(path)
36
+ File.readlines(path).map { |l| JSON.parse(l) }
37
+ end
38
+
39
+ describe "extract_corpus titles:" do
40
+ it "extracts an explicit set with normalization, dedup, and input order" do
41
+ out = File.join(@dir, "t.jsonl")
42
+ result = @corpus.extract_corpus(
43
+ output_path: out, content: "summary",
44
+ titles: ["Film B", "Film_A", "film A", "Film B"], num_processes: 0
45
+ )
46
+
47
+ expect(result[:total_matching]).to eq(2) # normalized + deduplicated
48
+ expect(result[:articles_extracted]).to eq(2)
49
+ expect(read_jsonl(out).map { |r| r["title"] }).to eq(["Film B", "Film A"])
50
+ expect(result[:not_found][:count]).to eq(0)
51
+ end
52
+
53
+ it "resolves one redirect hop and extracts under the resolved title" do
54
+ out = File.join(@dir, "t.jsonl")
55
+ result = @corpus.extract_corpus(
56
+ output_path: out, content: "summary", titles: ["Old Film"], num_processes: 0
57
+ )
58
+
59
+ expect(result[:articles_extracted]).to eq(1)
60
+ expect(read_jsonl(out).first["title"]).to eq("Film A")
61
+ end
62
+
63
+ it "reports missing titles in not_found (count + sample)" do
64
+ out = File.join(@dir, "t.jsonl")
65
+ result = @corpus.extract_corpus(
66
+ output_path: out, content: "summary",
67
+ titles: ["Film A", "Ghost One", "Ghost Two"], num_processes: 0
68
+ )
69
+
70
+ expect(result[:articles_extracted]).to eq(1)
71
+ expect(result[:not_found]).to eq({ count: 2, sample: ["Ghost One", "Ghost Two"] })
72
+ meta = JSON.parse(File.read(result[:meta_path]))
73
+ expect(meta["not_found"]["count"]).to eq(2)
74
+ end
75
+
76
+ it "counts a redirect whose target does not exist as not found" do
77
+ db = SQLite3::Database.new(@db_path)
78
+ db.execute("INSERT INTO pages (page_id, title, namespace, redirect_to, text_length) VALUES (99, 'Broken Redirect', 0, 'Nowhere', 0)")
79
+ db.close
80
+
81
+ out = File.join(@dir, "t.jsonl")
82
+ result = @corpus.extract_corpus(
83
+ output_path: out, content: "summary", titles: ["Broken Redirect"], num_processes: 0
84
+ )
85
+ expect(result[:not_found]).to eq({ count: 1, sample: ["Broken Redirect"] })
86
+ expect(result[:articles_extracted]).to eq(0)
87
+ end
88
+
89
+ it "does not flag truncation for missing titles when the cap is not reached" do
90
+ out = File.join(@dir, "t.jsonl")
91
+ result = @corpus.extract_corpus(
92
+ output_path: out, content: "summary",
93
+ titles: ["Film A", "Ghost One", "Ghost Two"], num_processes: 0
94
+ )
95
+
96
+ expect(result[:not_found][:count]).to eq(2)
97
+ expect(result[:articles_extracted]).to eq(1)
98
+ expect(result[:truncated]).to be false
99
+ end
100
+
101
+ it "deduplicates titles that resolve to the same article" do
102
+ db = SQLite3::Database.new(@db_path)
103
+ db.execute("INSERT INTO pages (page_id, title, namespace, redirect_to, text_length) VALUES (98, 'Ancient Film', 0, 'Film A', 0)")
104
+ db.close
105
+
106
+ # (a) two aliases redirecting to the same target
107
+ out1 = File.join(@dir, "t1.jsonl")
108
+ result1 = @corpus.extract_corpus(
109
+ output_path: out1, content: "summary",
110
+ titles: ["Old Film", "Ancient Film"], num_processes: 0
111
+ )
112
+ expect(result1[:articles_extracted]).to eq(1)
113
+ expect(read_jsonl(out1).map { |r| r["title"] }).to eq(["Film A"])
114
+
115
+ # (b) a direct title plus its redirect alias
116
+ out2 = File.join(@dir, "t2.jsonl")
117
+ result2 = @corpus.extract_corpus(
118
+ output_path: out2, content: "summary",
119
+ titles: ["Film A", "Old Film"], num_processes: 0
120
+ )
121
+ expect(result2[:articles_extracted]).to eq(1)
122
+ expect(read_jsonl(out2).map { |r| r["title"] }).to eq(["Film A"])
123
+ end
124
+
125
+ it "rejects combination with filter arguments (set is defined twice)" do
126
+ out = File.join(@dir, "t.jsonl")
127
+ [
128
+ { category: "Japanese films" },
129
+ { categories: ["Japanese films"] },
130
+ { category_match: "films" },
131
+ { title_match: "Film" }
132
+ ].each do |filter|
133
+ expect do
134
+ @corpus.extract_corpus(output_path: out, content: "summary", titles: ["Film A"], **filter)
135
+ end.to raise_error(ArgumentError, /query_sql/), "expected #{filter.keys.first} to conflict"
136
+ end
137
+ end
138
+
139
+ it "rejects more than 10,000 titles" do
140
+ expect do
141
+ @corpus.extract_corpus(output_path: File.join(@dir, "t.jsonl"), content: "summary",
142
+ titles: Array.new(10_001) { |i| "T#{i}" })
143
+ end.to raise_error(ArgumentError, /10_000|10000/)
144
+ end
145
+
146
+ it "interacts with limit and max_articles like the filter path" do
147
+ titles = ["Film A", "Film B", "Person X"]
148
+ limited = @corpus.extract_corpus(
149
+ output_path: File.join(@dir, "t1.jsonl"), content: "summary",
150
+ titles: titles, limit: 2, num_processes: 0
151
+ )
152
+ expect(limited[:articles_extracted]).to eq(2)
153
+ expect(limited[:truncated]).to be true
154
+ expect(limited[:total_matching]).to eq(3)
155
+
156
+ capped = @corpus.extract_corpus(
157
+ output_path: File.join(@dir, "t2.jsonl"), content: "summary",
158
+ titles: titles, max_articles: 1, num_processes: 0
159
+ )
160
+ expect(capped[:articles_extracted]).to eq(1)
161
+ expect(capped[:truncated]).to be true
162
+ end
163
+
164
+ it "enumerates titles in .meta.json when 100 or fewer, and the sha is order-independent" do
165
+ result1 = @corpus.extract_corpus(
166
+ output_path: File.join(@dir, "t1.jsonl"), content: "summary",
167
+ titles: ["Film A", "Film B"], num_processes: 0
168
+ )
169
+ result2 = @corpus.extract_corpus(
170
+ output_path: File.join(@dir, "t2.jsonl"), content: "summary",
171
+ titles: ["Film B", "Film_A"], num_processes: 0
172
+ )
173
+ meta1 = JSON.parse(File.read(result1[:meta_path]))
174
+ meta2 = JSON.parse(File.read(result2[:meta_path]))
175
+
176
+ expect(meta1["query"]["titles"]).to eq(["Film A", "Film B"])
177
+ expect(meta1["query"]["titles_count"]).to eq(2)
178
+ expect(meta1["query"]["titles_sha256"]).to eq(meta2["query"]["titles_sha256"])
179
+ end
180
+
181
+ it "records only count + sha256 in .meta.json when more than 100 titles" do
182
+ titles = ["Film A"] + Array.new(100) { |i| "Ghost #{i}" }
183
+ result = @corpus.extract_corpus(
184
+ output_path: File.join(@dir, "t.jsonl"), content: "summary",
185
+ titles: titles, num_processes: 0
186
+ )
187
+ meta = JSON.parse(File.read(result[:meta_path]))
188
+
189
+ expect(meta["query"]["titles_count"]).to eq(101)
190
+ expect(meta["query"]["titles_sha256"]).to match(/\A[0-9a-f]{64}\z/)
191
+ expect(meta["query"]).not_to have_key("titles")
192
+ end
193
+
194
+ it "passes titles through start_extract_job" do
195
+ out = File.join(@dir, "job.jsonl")
196
+ manager = Wp2txt::CorpusJobManager.new(
197
+ -> { Wp2txt::Corpus.for_input(@multistream_path, cache_dir: @dir) }
198
+ )
199
+ start = manager.start_extract(
200
+ output_path: out, content: "summary",
201
+ titles: ["Film A", "Ghost"], num_processes: 0
202
+ )
203
+
204
+ deadline = Time.now + 30
205
+ status = nil
206
+ loop do
207
+ status = manager.status(start[:job_id])
208
+ break unless status[:status] == "running"
209
+ raise "job did not finish in time" if Time.now > deadline
210
+
211
+ sleep 0.1
212
+ end
213
+
214
+ expect(status[:status]).to eq("completed")
215
+ expect(status[:result][:articles_extracted]).to eq(1)
216
+ expect(status[:result][:not_found][:count]).to eq(1)
217
+ end
218
+ end
219
+
220
+ describe "query_sql output_path:" do
221
+ it "writes all rows as JSONL with NULLs and deduplicated column names" do
222
+ out = File.join(@dir, "q.jsonl")
223
+ result = @corpus.query_sql(
224
+ "SELECT title, NULL AS note, page_id AS x, page_id AS x FROM pages " \
225
+ "WHERE namespace = 0 AND redirect_to IS NULL ORDER BY page_id",
226
+ output_path: out
227
+ )
228
+
229
+ expect(result[:columns]).to eq(["title", "note", "x", "x_2"])
230
+ expect(result[:row_count]).to eq(3)
231
+ expect(result[:sample].size).to eq(3)
232
+ expect(result).not_to have_key(:rows)
233
+
234
+ records = read_jsonl(out)
235
+ expect(records.size).to eq(3)
236
+ expect(records.first).to eq({ "title" => "Film A", "note" => nil, "x" => 1, "x_2" => 1 })
237
+ expect(File.exist?("#{out}.partial")).to be false
238
+ end
239
+
240
+ it "ignores limit in file-output mode" do
241
+ out = File.join(@dir, "q.jsonl")
242
+ result = @corpus.query_sql("SELECT title FROM pages", output_path: out, limit: 1)
243
+ expect(result[:row_count]).to eq(8)
244
+ expect(read_jsonl(out).size).to eq(8)
245
+ end
246
+
247
+ it "refuses an existing output file unless overwrite is set" do
248
+ out = File.join(@dir, "q.jsonl")
249
+ File.write(out, "old")
250
+ expect { @corpus.query_sql("SELECT 1", output_path: out) }
251
+ .to raise_error(ArgumentError, /already exists/)
252
+ expect(File.read(out)).to eq("old")
253
+
254
+ @corpus.query_sql("SELECT 1 AS one", output_path: out, overwrite: true)
255
+ expect(read_jsonl(out)).to eq([{ "one" => 1 }])
256
+ end
257
+
258
+ it "removes .partial and leaves no output on SQL error" do
259
+ out = File.join(@dir, "q.jsonl")
260
+ expect { @corpus.query_sql("SELECT * FROM no_such_table", output_path: out) }
261
+ .to raise_error(ArgumentError, /SQL error/)
262
+ expect(File.exist?(out)).to be false
263
+ expect(File.exist?("#{out}.partial")).to be false
264
+ end
265
+
266
+ it "removes .partial and leaves no output on timeout kill" do
267
+ out = File.join(@dir, "q.jsonl")
268
+ runaway = "WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM c) SELECT x FROM c"
269
+ expect { @corpus.query_sql(runaway, output_path: out, timeout: 1) }
270
+ .to raise_error(ArgumentError, /time limit/)
271
+ expect(File.exist?(out)).to be false
272
+ expect(File.exist?("#{out}.partial")).to be false
273
+ end
274
+
275
+ it "truncates at SQL_FILE_ROW_LIMIT" do
276
+ stub_const("Wp2txt::Corpus::SQL_FILE_ROW_LIMIT", 3)
277
+ out = File.join(@dir, "q.jsonl")
278
+ result = @corpus.query_sql("SELECT title FROM pages", output_path: out)
279
+
280
+ expect(result[:row_count]).to eq(3)
281
+ expect(result[:truncated]).to be true
282
+ expect(read_jsonl(out).size).to eq(3)
283
+ end
284
+
285
+ it "clips cells over 64KB and counts them" do
286
+ out = File.join(@dir, "q.jsonl")
287
+ result = @corpus.query_sql(
288
+ "SELECT printf('%.70000d', 0) AS big",
289
+ output_path: out
290
+ )
291
+
292
+ expect(result[:cells_clipped]).to eq(1)
293
+ value = read_jsonl(out).first["big"]
294
+ expect(value.length).to eq(Wp2txt::Corpus::SQL_FILE_CELL_LIMIT + 1)
295
+ end
296
+
297
+ it "writes a .meta.json sidecar with SQL, attach provenance, and counts" do
298
+ create_meta_db(@dir, lang: "en", date: "20260101",
299
+ pages: [[101, "Film A", 0, nil, 10]])
300
+ out = File.join(@dir, "q.jsonl")
301
+ result = @corpus.query_sql(
302
+ "SELECT COUNT(*) AS n FROM en_meta.pages", attach: ["en"], output_path: out
303
+ )
304
+
305
+ expect(result[:attached].first[:lang]).to eq("en")
306
+ meta = JSON.parse(File.read("#{out}.meta.json"))
307
+ expect(meta["tool"]).to eq("query_sql")
308
+ expect(meta["dump"]).to eq("testwiki-20260101")
309
+ expect(meta["sql"]).to include("en_meta.pages")
310
+ expect(meta["attached"]).to eq([
311
+ { "lang" => "en", "dump_name" => "enwiki-20260101", "built_with" => Wp2txt::VERSION }
312
+ ])
313
+ expect(meta["row_count"]).to eq(1)
314
+ expect(meta["truncated"]).to be false
315
+ expect(meta["cells_clipped"]).to eq(0)
316
+ expect(meta["generated_at"]).to match(/\A\d{4}-\d{2}-\d{2}T/)
317
+ expect(meta["wp2txt_version"]).to eq(Wp2txt::VERSION)
318
+ end
319
+ end
320
+
321
+ describe Wp2txt::OutputPath do
322
+ it "confines paths under the server output directory" do
323
+ expect(described_class.confine("sub/out.jsonl", @dir)).to eq(File.join(@dir, "sub/out.jsonl"))
324
+ expect { described_class.confine("../escape.jsonl", @dir) }
325
+ .to raise_error(ArgumentError, /output directory/)
326
+ expect { described_class.confine("/etc/passwd", @dir) }
327
+ .to raise_error(ArgumentError, /output directory/)
328
+ end
329
+
330
+ it "refuses existing files unless overwrite is set" do
331
+ existing = File.join(@dir, "exists.jsonl")
332
+ File.write(existing, "x")
333
+ expect { described_class.confine("exists.jsonl", @dir) }
334
+ .to raise_error(ArgumentError, /already exists/)
335
+ expect(described_class.confine("exists.jsonl", @dir, overwrite: true)).to eq(existing)
336
+ end
337
+ end
338
+ end
data/spec/utils_spec.rb CHANGED
@@ -59,6 +59,135 @@ RSpec.describe "Wp2txt Utils" do
59
59
  end
60
60
  end
61
61
 
62
+ # Regression: process_external_links used to strip the brackets of the
63
+ # [ref]/[/ref] markers (contents "ref" / "/ref" take the parts.size == 1
64
+ # branch), so remove_ref could no longer find them and the tag names plus
65
+ # reference body leaked into the extracted text. These tests exercise the
66
+ # composed make_reference -> format_wiki path, since testing remove_ref in
67
+ # isolation passes even with the bug.
68
+ describe "reference markers through format_wiki" do
69
+ it "removes <ref>...</ref> including the body" do
70
+ input = "Paris was founded.<ref>Patrick Boucheron, France in the World (2019) pp 81-86.</ref> The city grew."
71
+ result = format_wiki(make_reference(input))
72
+ expect(result).to eq "Paris was founded. The city grew."
73
+ end
74
+
75
+ it "removes named <ref name=\"x\">...</ref> including the body" do
76
+ input = "Fine dining.<ref name=\"lemonde\">Le Monde, 2 February 2015</ref> Paris has."
77
+ result = format_wiki(make_reference(input))
78
+ expect(result).to eq "Fine dining. Paris has."
79
+ end
80
+
81
+ it "removes self-closing <ref name=\"x\"/>" do
82
+ input = "An asteroid,<ref name=\"x\"/> and a building."
83
+ result = format_wiki(make_reference(input))
84
+ expect(result).to eq "An asteroid, and a building."
85
+ end
86
+
87
+ it "removes consecutive <ref>A</ref><ref>B</ref> without joining tag names" do
88
+ input = "Text<ref>A</ref><ref>B</ref> more."
89
+ result = format_wiki(make_reference(input))
90
+ expect(result).to eq "Text more."
91
+ end
92
+
93
+ it "keeps [ref]...[/ref] markers when config[:ref] is true" do
94
+ input = "Paris was founded.<ref>Patrick Boucheron, France in the World (2019) pp 81-86.</ref> The city grew."
95
+ result = format_wiki(make_reference(input), ref: true)
96
+ expect(result).to eq "Paris was founded.[ref]Patrick Boucheron, France in the World (2019) pp 81-86.[/ref] The city grew."
97
+ end
98
+
99
+ # Guards against the rejected fix of returning "[ref]" from the scanner
100
+ # block: that makes process_nested_single_pass re-detect the same spot
101
+ # until MAX_NESTING_ITERATIONS, leaving external links unprocessed.
102
+ it "still processes external links outside [ref] markers" do
103
+ input = "Claim.<ref>See [http://example.com the site] for detail.</ref> Next [http://foo.com Foo] end."
104
+ expect(format_wiki(make_reference(input))).to eq "Claim. Next Foo end."
105
+ expect(format_wiki(make_reference(input), ref: true)).to eq "Claim.[ref]See the site for detail.[/ref] Next Foo end."
106
+ end
107
+ end
108
+
109
+ # Regression: element splitting (Article#parse) breaks paragraphs at
110
+ # newlines, so a reference written across lines landed in separate elements
111
+ # with [ref] and [/ref] never visible to remove_ref at the same time. This
112
+ # also meant --extract-citations never fired for multi-line cite templates.
113
+ # These tests go through Article.new -> format_wiki per element, since
114
+ # passing a string to format_wiki directly skips element splitting and does
115
+ # not reproduce the bug.
116
+ describe "multi-line references through element splitting" do
117
+ def render_elements(wikitext, config = {})
118
+ Wp2txt::Article.new(wikitext).elements.map { |e| format_wiki(e[1], config) }.join
119
+ end
120
+
121
+ it "removes an empty reference spanning a blank line" do
122
+ result = render_elements("...in the Super League.<ref>\n\n</ref> In 2006, Catalans Dragons became...")
123
+ expect(result).not_to include("[ref]")
124
+ expect(result).not_to include("[/ref]")
125
+ expect(result).to include("Super League. In 2006,")
126
+ end
127
+
128
+ it "removes a named empty reference spanning a blank line" do
129
+ result = render_elements("Claim.<ref name=\"x\">\n\n</ref> Next.")
130
+ expect(result).not_to include("[ref]")
131
+ expect(result).not_to include("[/ref]")
132
+ expect(result).to include("Claim. Next.")
133
+ end
134
+
135
+ it "removes a non-empty reference spanning a blank line" do
136
+ result = render_elements("Claim.<ref>Author\n\nPublisher</ref> Next.")
137
+ expect(result).not_to include("[ref]")
138
+ expect(result).not_to include("[/ref]")
139
+ expect(result).to include("Claim. Next.")
140
+ end
141
+
142
+ it "removes a multi-line cite template reference (with and without leading space)" do
143
+ ["Claim.<ref> {{cite book\n |title=Foo\n |year=2019}}</ref> Next.",
144
+ "Claim.<ref>{{cite book\n|title=Foo\n|year=2019}}</ref> Next."].each do |input|
145
+ result = render_elements(input)
146
+ expect(result).not_to include("[ref]")
147
+ expect(result).not_to include("[/ref]")
148
+ expect(result).not_to include("cite book")
149
+ expect(result).to include("Claim. Next.")
150
+ end
151
+ end
152
+
153
+ # The core of this fix: before, only the multi-line form leaked raw
154
+ # markup, so --extract-citations silently did nothing for it.
155
+ it "extracts citations identically from single-line and multi-line cite templates" do
156
+ config = { extract_citations: true, ref: true }
157
+ single = render_elements("Claim.<ref>{{cite book|title=Foo|year=2019}}</ref> Next.", config)
158
+ multi = render_elements("Claim.<ref>{{cite book\n|title=Foo\n|year=2019}}</ref> Next.", config)
159
+ expect(multi).to eq single
160
+ expect(single).to include("[ref]\"Foo\". 2019.[/ref]")
161
+ end
162
+
163
+ # Control: newlines outside references must still split paragraphs.
164
+ it "still splits ordinary paragraph boundaries at blank lines" do
165
+ elements = Wp2txt::Article.new("First para.\n\nSecond para.").elements
166
+ paragraphs = elements.select { |e| e[0] == :mw_paragraph }
167
+ expect(paragraphs.size).to eq 2
168
+ expect(paragraphs[0][1]).to include("First para.")
169
+ expect(paragraphs[1][1]).to include("Second para.")
170
+ end
171
+
172
+ # An unclosed <ref> must not pair with a later </ref> across paragraphs;
173
+ # the body text stays (a floating [ref] is the pre-existing behavior for
174
+ # this malformed markup).
175
+ it "does not swallow paragraphs when a <ref> is left unclosed" do
176
+ wikitext = "First para with <ref>unclosed reference.\n\nSecond para here.\n\n" \
177
+ "Third para with <ref>closed</ref> end."
178
+ result = render_elements(wikitext)
179
+ expect(result).to include("Second para here.")
180
+ expect(result).to include("First para with")
181
+ end
182
+
183
+ it "handles a multi-line reference with a group attribute" do
184
+ result = render_elements("Claim.<ref group=\"note\">Author\nTitle 2019</ref> Next.")
185
+ expect(result).not_to include("[ref]")
186
+ expect(result).not_to include("[/ref]")
187
+ expect(result).to include("Claim. Next.")
188
+ end
189
+ end
190
+
62
191
  describe "remove_table" do
63
192
  it "removes table formated parts" do
64
193
  str_before = "{| ... \n{| ... \n ...|}\n ...|}"
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.2.0
4
+ version: 2.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoichiro Hasebe
@@ -217,6 +217,7 @@ files:
217
217
  - Rakefile
218
218
  - bin/wp2txt
219
219
  - bin/wp2txt-mcp
220
+ - docs/RESEARCH.md
220
221
  - image/wp2txt-logo.svg
221
222
  - image/wp2txt.svg
222
223
  - lib/wp2txt.rb
@@ -242,10 +243,12 @@ files:
242
243
  - lib/wp2txt/global_data_cache.rb
243
244
  - lib/wp2txt/index_cache.rb
244
245
  - lib/wp2txt/index_commands.rb
246
+ - lib/wp2txt/langlinks_importer.rb
245
247
  - lib/wp2txt/magic_words.rb
246
248
  - lib/wp2txt/memory_monitor.rb
247
249
  - lib/wp2txt/metadata_index.rb
248
250
  - lib/wp2txt/multistream.rb
251
+ - lib/wp2txt/output_path.rb
249
252
  - lib/wp2txt/output_writer.rb
250
253
  - lib/wp2txt/parser_functions.rb
251
254
  - lib/wp2txt/ractor_worker.rb
@@ -273,6 +276,7 @@ files:
273
276
  - spec/config_spec.rb
274
277
  - spec/constants_spec.rb
275
278
  - spec/corpus_spec.rb
279
+ - spec/docs_sync_spec.rb
276
280
  - spec/file_utils_spec.rb
277
281
  - spec/fixtures/samples.rb
278
282
  - spec/formatter_sections_spec.rb
@@ -280,10 +284,12 @@ files:
280
284
  - spec/global_data_cache_spec.rb
281
285
  - spec/index_cache_spec.rb
282
286
  - spec/integration_spec.rb
287
+ - spec/langlinks_importer_spec.rb
283
288
  - spec/magic_words_spec.rb
284
289
  - spec/markers_spec.rb
285
290
  - spec/memory_monitor_spec.rb
286
291
  - spec/metadata_index_spec.rb
292
+ - spec/multi_dump_attach_spec.rb
287
293
  - spec/multistream_spec.rb
288
294
  - spec/output_writer_spec.rb
289
295
  - spec/parser_functions_spec.rb
@@ -292,11 +298,13 @@ files:
292
298
  - spec/section_extractor_spec.rb
293
299
  - spec/spec_helper.rb
294
300
  - spec/stream_processor_spec.rb
301
+ - spec/support/meta_db_fixture.rb
295
302
  - spec/support/multistream_fixture.rb
296
303
  - spec/template_data_spec.rb
297
304
  - spec/template_expander_spec.rb
298
305
  - spec/template_processing_spec.rb
299
306
  - spec/text_processing_spec.rb
307
+ - spec/titles_output_path_spec.rb
300
308
  - spec/utils_spec.rb
301
309
  - spec/wp2txt_spec.rb
302
310
  - wp2txt.gemspec
@@ -334,6 +342,7 @@ test_files:
334
342
  - spec/config_spec.rb
335
343
  - spec/constants_spec.rb
336
344
  - spec/corpus_spec.rb
345
+ - spec/docs_sync_spec.rb
337
346
  - spec/file_utils_spec.rb
338
347
  - spec/fixtures/samples.rb
339
348
  - spec/formatter_sections_spec.rb
@@ -341,10 +350,12 @@ test_files:
341
350
  - spec/global_data_cache_spec.rb
342
351
  - spec/index_cache_spec.rb
343
352
  - spec/integration_spec.rb
353
+ - spec/langlinks_importer_spec.rb
344
354
  - spec/magic_words_spec.rb
345
355
  - spec/markers_spec.rb
346
356
  - spec/memory_monitor_spec.rb
347
357
  - spec/metadata_index_spec.rb
358
+ - spec/multi_dump_attach_spec.rb
348
359
  - spec/multistream_spec.rb
349
360
  - spec/output_writer_spec.rb
350
361
  - spec/parser_functions_spec.rb
@@ -353,10 +364,12 @@ test_files:
353
364
  - spec/section_extractor_spec.rb
354
365
  - spec/spec_helper.rb
355
366
  - spec/stream_processor_spec.rb
367
+ - spec/support/meta_db_fixture.rb
356
368
  - spec/support/multistream_fixture.rb
357
369
  - spec/template_data_spec.rb
358
370
  - spec/template_expander_spec.rb
359
371
  - spec/template_processing_spec.rb
360
372
  - spec/text_processing_spec.rb
373
+ - spec/titles_output_path_spec.rb
361
374
  - spec/utils_spec.rb
362
375
  - spec/wp2txt_spec.rb