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
data/scripts/benchmark_regex.rb
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Benchmark script for wp2txt regex performance
|
|
4
|
-
# Compares pre-compiled regex patterns vs inline compilation
|
|
5
|
-
#
|
|
6
|
-
# Usage: ruby scripts/benchmark_regex.rb
|
|
7
|
-
|
|
8
|
-
require "benchmark"
|
|
9
|
-
begin
|
|
10
|
-
require "benchmark/ips"
|
|
11
|
-
rescue LoadError
|
|
12
|
-
# benchmark-ips is optional
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Add lib to load path
|
|
16
|
-
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
17
|
-
require "wp2txt"
|
|
18
|
-
require "wp2txt/article"
|
|
19
|
-
|
|
20
|
-
# Sample Wikipedia-like content for benchmarking
|
|
21
|
-
SAMPLE_TEXT = <<~WIKI
|
|
22
|
-
{{Infobox person
|
|
23
|
-
| name = Test Person
|
|
24
|
-
| birth_date = 1980-01-01
|
|
25
|
-
| occupation = Writer
|
|
26
|
-
}}
|
|
27
|
-
'''Test Article''' is a [[test]] article with various [[wiki markup|markup]].
|
|
28
|
-
|
|
29
|
-
== Section 1 ==
|
|
30
|
-
This section has some {{cite web|url=http://example.com|title=Example}} references.
|
|
31
|
-
There are also [[Category:Test]] links and [[File:Image.jpg|thumb|A caption]].
|
|
32
|
-
|
|
33
|
-
=== Subsection ===
|
|
34
|
-
More content with '''bold''' and ''italic'' text.
|
|
35
|
-
* List item 1
|
|
36
|
-
* List item 2
|
|
37
|
-
# Numbered item
|
|
38
|
-
|
|
39
|
-
== Section 2 ==
|
|
40
|
-
{| class="wikitable"
|
|
41
|
-
|-
|
|
42
|
-
! Header 1 !! Header 2
|
|
43
|
-
|-
|
|
44
|
-
| Cell 1 || Cell 2
|
|
45
|
-
|}
|
|
46
|
-
|
|
47
|
-
Some text with entities and ♪ characters.
|
|
48
|
-
Also has <ref name="test">Reference content</ref> and <nowiki>[[preserved]]</nowiki>.
|
|
49
|
-
|
|
50
|
-
{{DEFAULTSORT:Test Article}}
|
|
51
|
-
[[Category:Articles]]
|
|
52
|
-
[[Category:Tests]]
|
|
53
|
-
WIKI
|
|
54
|
-
|
|
55
|
-
# Create multiple copies for more realistic benchmarking
|
|
56
|
-
LARGE_TEXT = (SAMPLE_TEXT * 100).freeze
|
|
57
|
-
|
|
58
|
-
class BenchmarkRunner
|
|
59
|
-
include Wp2txt
|
|
60
|
-
|
|
61
|
-
def initialize
|
|
62
|
-
@nowikis = {}
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def run_cleanup(text)
|
|
66
|
-
cleanup(text.dup)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def run_full_format(text)
|
|
70
|
-
format_wiki(text.dup)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def run_benchmarks
|
|
75
|
-
puts "=" * 60
|
|
76
|
-
puts "wp2txt Regex Performance Benchmark"
|
|
77
|
-
puts "=" * 60
|
|
78
|
-
puts
|
|
79
|
-
puts "Ruby version: #{RUBY_VERSION}"
|
|
80
|
-
puts "Sample text size: #{SAMPLE_TEXT.bytesize} bytes"
|
|
81
|
-
puts "Large text size: #{LARGE_TEXT.bytesize} bytes"
|
|
82
|
-
puts
|
|
83
|
-
|
|
84
|
-
runner = BenchmarkRunner.new
|
|
85
|
-
|
|
86
|
-
puts "-" * 60
|
|
87
|
-
puts "Warmup (JIT compilation, method caching)"
|
|
88
|
-
puts "-" * 60
|
|
89
|
-
5.times { runner.run_cleanup(SAMPLE_TEXT) }
|
|
90
|
-
5.times { runner.run_full_format(SAMPLE_TEXT) }
|
|
91
|
-
puts "Done."
|
|
92
|
-
puts
|
|
93
|
-
|
|
94
|
-
puts "-" * 60
|
|
95
|
-
puts "Benchmark: cleanup() method"
|
|
96
|
-
puts "-" * 60
|
|
97
|
-
|
|
98
|
-
Benchmark.bm(20) do |x|
|
|
99
|
-
x.report("cleanup (small):") do
|
|
100
|
-
1000.times { runner.run_cleanup(SAMPLE_TEXT) }
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
x.report("cleanup (large):") do
|
|
104
|
-
10.times { runner.run_cleanup(LARGE_TEXT) }
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
puts
|
|
109
|
-
puts "-" * 60
|
|
110
|
-
puts "Benchmark: format_wiki() method (full pipeline)"
|
|
111
|
-
puts "-" * 60
|
|
112
|
-
|
|
113
|
-
Benchmark.bm(20) do |x|
|
|
114
|
-
x.report("format_wiki (small):") do
|
|
115
|
-
1000.times { runner.run_full_format(SAMPLE_TEXT) }
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
x.report("format_wiki (large):") do
|
|
119
|
-
10.times { runner.run_full_format(LARGE_TEXT) }
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# If benchmark-ips is available, run IPS benchmarks
|
|
124
|
-
if defined?(Benchmark::IPS)
|
|
125
|
-
puts
|
|
126
|
-
puts "-" * 60
|
|
127
|
-
puts "IPS Benchmark (iterations per second)"
|
|
128
|
-
puts "-" * 60
|
|
129
|
-
|
|
130
|
-
Benchmark.ips do |x|
|
|
131
|
-
x.report("cleanup") { runner.run_cleanup(SAMPLE_TEXT) }
|
|
132
|
-
x.report("format_wiki") { runner.run_full_format(SAMPLE_TEXT) }
|
|
133
|
-
x.compare!
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
puts
|
|
138
|
-
puts "-" * 60
|
|
139
|
-
puts "Memory profile (approximate)"
|
|
140
|
-
puts "-" * 60
|
|
141
|
-
|
|
142
|
-
# Simple memory measurement
|
|
143
|
-
GC.start
|
|
144
|
-
before = GC.stat[:total_allocated_objects]
|
|
145
|
-
100.times { runner.run_cleanup(SAMPLE_TEXT) }
|
|
146
|
-
after = GC.stat[:total_allocated_objects]
|
|
147
|
-
puts "cleanup() allocations per call: ~#{(after - before) / 100}"
|
|
148
|
-
|
|
149
|
-
GC.start
|
|
150
|
-
before = GC.stat[:total_allocated_objects]
|
|
151
|
-
100.times { runner.run_full_format(SAMPLE_TEXT) }
|
|
152
|
-
after = GC.stat[:total_allocated_objects]
|
|
153
|
-
puts "format_wiki() allocations per call: ~#{(after - before) / 100}"
|
|
154
|
-
|
|
155
|
-
puts
|
|
156
|
-
puts "=" * 60
|
|
157
|
-
puts "Benchmark complete"
|
|
158
|
-
puts "=" * 60
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
run_benchmarks
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Fetches HTML named character references from WHATWG HTML specification
|
|
4
|
-
# Usage: ruby scripts/fetch_html_entities.rb
|
|
5
|
-
#
|
|
6
|
-
# This script downloads the official entities.json from WHATWG and converts
|
|
7
|
-
# it into a format suitable for wp2txt text processing.
|
|
8
|
-
|
|
9
|
-
require "net/http"
|
|
10
|
-
require "json"
|
|
11
|
-
require "fileutils"
|
|
12
|
-
|
|
13
|
-
WHATWG_ENTITIES_URL = "https://html.spec.whatwg.org/entities.json"
|
|
14
|
-
|
|
15
|
-
def fetch_whatwg_entities
|
|
16
|
-
puts "Fetching entities from WHATWG HTML specification..."
|
|
17
|
-
uri = URI(WHATWG_ENTITIES_URL)
|
|
18
|
-
|
|
19
|
-
response = Net::HTTP.get_response(uri)
|
|
20
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
21
|
-
warn "Failed to fetch entities: HTTP #{response.code}"
|
|
22
|
-
return nil
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
JSON.parse(response.body)
|
|
26
|
-
rescue StandardError => e
|
|
27
|
-
warn "Error fetching entities: #{e.message}"
|
|
28
|
-
nil
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def convert_entities(raw_data)
|
|
32
|
-
entities = {}
|
|
33
|
-
|
|
34
|
-
raw_data.each do |name, info|
|
|
35
|
-
# Only include entries with semicolon (standard form)
|
|
36
|
-
# Skip legacy forms without semicolon like " "
|
|
37
|
-
next unless name.end_with?(";")
|
|
38
|
-
|
|
39
|
-
# Extract entity name without & and ;
|
|
40
|
-
# e.g., "α" -> "alpha"
|
|
41
|
-
key = name
|
|
42
|
-
|
|
43
|
-
# Get the character(s)
|
|
44
|
-
characters = info["characters"]
|
|
45
|
-
next if characters.nil? || characters.empty?
|
|
46
|
-
|
|
47
|
-
entities[key] = characters
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
entities
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def main
|
|
54
|
-
raw_data = fetch_whatwg_entities
|
|
55
|
-
if raw_data.nil?
|
|
56
|
-
warn "Failed to fetch entities. Aborting."
|
|
57
|
-
exit 1
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
puts "Processing #{raw_data.size} raw entries..."
|
|
61
|
-
|
|
62
|
-
entities = convert_entities(raw_data)
|
|
63
|
-
puts "Converted to #{entities.size} standard entities (with semicolon)"
|
|
64
|
-
|
|
65
|
-
result = {
|
|
66
|
-
"meta" => {
|
|
67
|
-
"generated_at" => Time.now.utc.iso8601,
|
|
68
|
-
"source" => WHATWG_ENTITIES_URL,
|
|
69
|
-
"description" => "HTML named character references from WHATWG HTML specification",
|
|
70
|
-
"total_entities" => entities.size
|
|
71
|
-
},
|
|
72
|
-
"entities" => entities.sort.to_h
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
# Write output
|
|
76
|
-
output_path = File.join(__dir__, "..", "lib", "wp2txt", "data", "html_entities.json")
|
|
77
|
-
FileUtils.mkdir_p(File.dirname(output_path))
|
|
78
|
-
|
|
79
|
-
File.write(output_path, JSON.pretty_generate(result))
|
|
80
|
-
puts "\nData written to: #{output_path}"
|
|
81
|
-
|
|
82
|
-
# Summary - show some categories
|
|
83
|
-
greek = entities.keys.select { |k| k.match?(/&(alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega);/i) }
|
|
84
|
-
math = entities.keys.select { |k| k.match?(/&(sum|prod|int|infin|nabla|part|forall|exist|empty|isin|notin|cap|cup|sub|sup|oplus|otimes);/i) }
|
|
85
|
-
arrows = entities.keys.select { |k| k.match?(/arr;$/i) }
|
|
86
|
-
|
|
87
|
-
puts "\n=== Summary ==="
|
|
88
|
-
puts "Total entities: #{entities.size}"
|
|
89
|
-
puts "Greek letters: #{greek.size}"
|
|
90
|
-
puts "Math symbols: #{math.size}"
|
|
91
|
-
puts "Arrows: #{arrows.size}"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
main if __FILE__ == $PROGRAM_NAME
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Fetches Wikipedia language metadata from Wikimedia APIs
|
|
4
|
-
# Usage: ruby scripts/fetch_language_metadata.rb
|
|
5
|
-
#
|
|
6
|
-
# This script queries the Wikimedia sitematrix API to get all Wikipedia
|
|
7
|
-
# language editions and their statistics (article counts, etc.)
|
|
8
|
-
|
|
9
|
-
require "net/http"
|
|
10
|
-
require "json"
|
|
11
|
-
require "fileutils"
|
|
12
|
-
|
|
13
|
-
# Fetch all Wikipedia languages with statistics from sitematrix API
|
|
14
|
-
def fetch_wikipedia_languages
|
|
15
|
-
uri = URI("https://meta.wikimedia.org/w/api.php")
|
|
16
|
-
params = {
|
|
17
|
-
action: "sitematrix",
|
|
18
|
-
smtype: "language",
|
|
19
|
-
format: "json"
|
|
20
|
-
}
|
|
21
|
-
uri.query = URI.encode_www_form(params)
|
|
22
|
-
|
|
23
|
-
response = Net::HTTP.get_response(uri)
|
|
24
|
-
return {} unless response.is_a?(Net::HTTPSuccess)
|
|
25
|
-
|
|
26
|
-
data = JSON.parse(response.body)
|
|
27
|
-
languages = {}
|
|
28
|
-
|
|
29
|
-
data["sitematrix"].each do |key, val|
|
|
30
|
-
next unless key.match?(/^\d+$/) && val.is_a?(Hash) && val["site"]
|
|
31
|
-
|
|
32
|
-
# Find Wikipedia site info
|
|
33
|
-
wiki_site = val["site"].find { |site| site["code"] == "wiki" }
|
|
34
|
-
next unless wiki_site
|
|
35
|
-
|
|
36
|
-
lang_code = val["code"]
|
|
37
|
-
languages[lang_code] = {
|
|
38
|
-
"name" => val["name"],
|
|
39
|
-
"localname" => val["localname"],
|
|
40
|
-
"url" => wiki_site["url"],
|
|
41
|
-
"dbname" => wiki_site["dbname"],
|
|
42
|
-
"closed" => wiki_site["closed"] || false,
|
|
43
|
-
"private" => wiki_site["private"] || false
|
|
44
|
-
}
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
languages
|
|
48
|
-
rescue StandardError => e
|
|
49
|
-
warn "Error fetching sitematrix: #{e.message}"
|
|
50
|
-
{}
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Fetch article statistics for a specific Wikipedia
|
|
54
|
-
def fetch_wiki_statistics(lang_code)
|
|
55
|
-
uri = URI("https://#{lang_code}.wikipedia.org/w/api.php")
|
|
56
|
-
params = {
|
|
57
|
-
action: "query",
|
|
58
|
-
meta: "siteinfo",
|
|
59
|
-
siprop: "statistics",
|
|
60
|
-
format: "json"
|
|
61
|
-
}
|
|
62
|
-
uri.query = URI.encode_www_form(params)
|
|
63
|
-
|
|
64
|
-
response = Net::HTTP.get_response(uri)
|
|
65
|
-
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
66
|
-
|
|
67
|
-
data = JSON.parse(response.body)
|
|
68
|
-
stats = data.dig("query", "statistics")
|
|
69
|
-
return nil unless stats
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
"articles" => stats["articles"],
|
|
73
|
-
"pages" => stats["pages"],
|
|
74
|
-
"edits" => stats["edits"],
|
|
75
|
-
"users" => stats["users"],
|
|
76
|
-
"activeusers" => stats["activeusers"]
|
|
77
|
-
}
|
|
78
|
-
rescue StandardError
|
|
79
|
-
nil
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def main
|
|
83
|
-
puts "Fetching Wikipedia language list..."
|
|
84
|
-
languages = fetch_wikipedia_languages
|
|
85
|
-
|
|
86
|
-
if languages.empty?
|
|
87
|
-
warn "Failed to fetch language list. Aborting."
|
|
88
|
-
exit 1
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Filter out closed/private wikis
|
|
92
|
-
active_languages = languages.reject { |_, info| info["closed"] || info["private"] }
|
|
93
|
-
puts "Found #{active_languages.size} active Wikipedia editions."
|
|
94
|
-
|
|
95
|
-
puts "Fetching statistics for each Wikipedia (this may take a few minutes)..."
|
|
96
|
-
successful = 0
|
|
97
|
-
failed = []
|
|
98
|
-
|
|
99
|
-
active_languages.each_with_index do |(lang_code, info), idx|
|
|
100
|
-
print "\r Processing: #{lang_code.ljust(10)} (#{idx + 1}/#{active_languages.size})"
|
|
101
|
-
$stdout.flush
|
|
102
|
-
|
|
103
|
-
stats = fetch_wiki_statistics(lang_code)
|
|
104
|
-
if stats
|
|
105
|
-
info.merge!(stats)
|
|
106
|
-
successful += 1
|
|
107
|
-
else
|
|
108
|
-
failed << lang_code
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
sleep 0.05 # Rate limiting
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
puts "\n Successfully fetched: #{successful}/#{active_languages.size}"
|
|
115
|
-
puts " Failed: #{failed.size} (#{failed.first(10).join(', ')}#{failed.size > 10 ? '...' : ''})" if failed.any?
|
|
116
|
-
|
|
117
|
-
# Categorize by size
|
|
118
|
-
size_categories = {
|
|
119
|
-
"large" => [], # 1M+ articles
|
|
120
|
-
"medium" => [], # 100K-1M articles
|
|
121
|
-
"small" => [], # 10K-100K articles
|
|
122
|
-
"mini" => [] # <10K articles
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
active_languages.each do |lang_code, info|
|
|
126
|
-
articles = info["articles"] || 0
|
|
127
|
-
category = if articles >= 1_000_000
|
|
128
|
-
"large"
|
|
129
|
-
elsif articles >= 100_000
|
|
130
|
-
"medium"
|
|
131
|
-
elsif articles >= 10_000
|
|
132
|
-
"small"
|
|
133
|
-
else
|
|
134
|
-
"mini"
|
|
135
|
-
end
|
|
136
|
-
size_categories[category] << lang_code
|
|
137
|
-
info["size_category"] = category
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Build result
|
|
141
|
-
result = {
|
|
142
|
-
"meta" => {
|
|
143
|
-
"generated_at" => Time.now.utc.iso8601,
|
|
144
|
-
"source" => "Wikimedia sitematrix + siteinfo APIs",
|
|
145
|
-
"total_languages" => active_languages.size,
|
|
146
|
-
"statistics_fetched" => successful
|
|
147
|
-
},
|
|
148
|
-
"size_summary" => {
|
|
149
|
-
"large" => size_categories["large"].size,
|
|
150
|
-
"medium" => size_categories["medium"].size,
|
|
151
|
-
"small" => size_categories["small"].size,
|
|
152
|
-
"mini" => size_categories["mini"].size
|
|
153
|
-
},
|
|
154
|
-
"languages" => active_languages.sort_by { |_, info| -(info["articles"] || 0) }.to_h
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
# Write output
|
|
158
|
-
output_path = File.join(__dir__, "..", "lib", "wp2txt", "data", "language_metadata.json")
|
|
159
|
-
FileUtils.mkdir_p(File.dirname(output_path))
|
|
160
|
-
|
|
161
|
-
File.write(output_path, JSON.pretty_generate(result))
|
|
162
|
-
puts "\nData written to: #{output_path}"
|
|
163
|
-
|
|
164
|
-
# Summary
|
|
165
|
-
puts "\n=== Summary ==="
|
|
166
|
-
puts "Total active Wikipedias: #{active_languages.size}"
|
|
167
|
-
puts "Size categories:"
|
|
168
|
-
puts " Large (1M+ articles): #{size_categories['large'].size} - #{size_categories['large'].first(5).join(', ')}..."
|
|
169
|
-
puts " Medium (100K-1M): #{size_categories['medium'].size}"
|
|
170
|
-
puts " Small (10K-100K): #{size_categories['small'].size}"
|
|
171
|
-
puts " Mini (<10K): #{size_categories['mini'].size}"
|
|
172
|
-
|
|
173
|
-
# Top 20 by article count
|
|
174
|
-
puts "\nTop 20 Wikipedias by article count:"
|
|
175
|
-
active_languages.sort_by { |_, info| -(info["articles"] || 0) }.first(20).each_with_index do |(code, info), idx|
|
|
176
|
-
puts " #{(idx + 1).to_s.rjust(2)}. #{code.ljust(5)} #{info['name'].to_s.ljust(20)} #{(info['articles'] || 0).to_s.rjust(10)} articles"
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
main if __FILE__ == $PROGRAM_NAME
|