跳转到内容

模組:PythonScript/Import

维基百科,自由的百科全书
-- Module:PythonScript/Import
-- 支援子頁面階層式套件 (__init__.py 與隱式命名空間套件)、相對匯入 (from . import ...)、
-- 動態 sys.path 尋址與兩階段註冊防循環引用的匯入引擎
local Import = {}

local SysRegistry = require("Module:PythonScript/Librarys/Argparse").Sys
local Runtime = require("Module:PythonScript/Runtime")

-- 不支援作為 Python 執行的 Content Model 黑名單
local UNSUPPORTED_CONTENT_MODELS = {
    javascript = "javascript",
    css = "css",
    ["sanitized-css"] = "sanitized-css",
    json = "json"
}

local UNSUPPORTED_EXT_MODELS = {
    js = "javascript",
    css = "css",
    json = "json"
}

-- 輔助函式:從 Wikitext 中精準提取 Python 程式碼,並回傳 (code, has_py_tag, non_py_lang)
local function extract_python_code(content)
    if not content then return "", false, nil end
    local code = content:match("<syntaxhighlight[^<>]+lang=['\"]?[Pp]ython3?['\"]?[^<>]*>(.-)</syntaxhighlight>")
    if not code then
        code = content:match("<source[^<>]+lang=['\"]?[Pp]ython3?['\"]?[^<>]*>(.-)</source>")
    end
    if code then
        return code, true, nil
    end

    -- 若未包含 Python 標籤,但明確標註了其他程式語言的 syntaxhighlight / source 標籤,直接識別為該語言
    local other_lang = content:match("<syntaxhighlight[^<>]+lang=['\"]?([%w_%-]+)['\"]?[^<>]*>")
        or content:match("<source[^<>]+lang=['\"]?([%w_%-]+)['\"]?[^<>]*>")
    if other_lang then
        local low = other_lang:lower()
        if low ~= "python" and low ~= "py" and low ~= "python3" then
            return "", false, low
        end
    end

    return content, false, nil
end

-- 零誤殺快速特徵預檢:僅針對模糊地帶頁面,秒殺 100% 不可能為合法 Python 開頭的 Wikitext / Lua / JS 標記
local function is_obvious_non_python(code)
    local first_line = code:match("^[\r\n%s]*([^\r\n]+)")
    if not first_line then return false end
    if first_line:match("^==+[^=]")
        or first_line:match("^{|")
        or first_line:match("^local%s+[%a_][%w_]*%s*=")
        or first_line:match("^/%*")
        or first_line:match("^<!DOCTYPE")
        or first_line:match("^<[Hh][Tt][Mm][Ll]") then
        return true
    end
    return false
end

-- 將全形底線「_」(U+FF3F) 正規化為標準半形底線「_」
local function to_halfwidth_underscore(s)
    if type(s) ~= "string" then return s end
    return (s:gsub("_", "_"))
end

-- 產生半形「_」與全形「_」的候選標題清單 (解決 MediaWiki 自動合併連續半形底線導致無法建立 __init__.py 的限制)
local function build_title_variants(title_str)
    local variants = {}
    local seen = {}
    local function add_v(v)
        if v and v ~= "" and not seen[v] then
            seen[v] = true
            table.insert(variants, v)
        end
    end

    local half = to_halfwidth_underscore(title_str)
    add_v(half:gsub("__+", function(m) return string.rep("_", #m) end))
    add_v(half:gsub("([^/:]+)$", function(seg) return (seg:gsub("_", "_")) end))
    add_v(title_str)
    add_v(half)
    add_v((half:gsub("_", "_")))
    add_v((half:gsub("__+", "_")))

    return variants
end

-- 頁面探針快取:避免重複查詢相同標題
local page_probe_cache = {}

local function probe_wiki_page(title_str)
    if not title_str or title_str == "" then return nil end
    local half_key = to_halfwidth_underscore(title_str)
    if page_probe_cache[title_str] ~= nil then
        return page_probe_cache[title_str] or nil
    end
    if page_probe_cache[half_key] ~= nil then
        return page_probe_cache[half_key] or nil
    end

    -- 第一關 (A):副檔名快速秒殺 (.js, .css, .json)
    local ext = half_key:match("%.([a-zA-Z0-9]+)$")
    if ext and UNSUPPORTED_EXT_MODELS[ext:lower()] then
        local info = {
            prefixedText = half_key,
            is_invalid_model = true,
            content_model = UNSUPPORTED_EXT_MODELS[ext:lower()]
        }
        page_probe_cache[title_str] = info
        page_probe_cache[half_key] = info
        return info
    end

    local candidates = build_title_variants(title_str)
    for _, cand_str in ipairs(candidates) do
        local ok, t = pcall(mw.title.new, cand_str)
        if ok and t and rawget(t, "exists") ~= false then
            local ok_c, raw_content = pcall(function() return t:getContent() end)
            if ok_c and raw_content then
                local ok_r, redir = pcall(function() return t.redirectTarget end)
                if ok_r and redir then
                    t = redir
                    raw_content = t:getContent() or raw_content
                end

                local norm_prefixed = to_halfwidth_underscore(t.prefixedText or cand_str)

                -- 第一關 (B):檢查 MediaWiki 頁面 Content Model (秒殺 javascript / css / json)
                local ok_cm, cm = pcall(function() return t.contentModel end)
                local cm_low = (ok_cm and type(cm) == "string") and cm:lower() or "wikitext"
                if UNSUPPORTED_CONTENT_MODELS[cm_low] then
                    local info = {
                        title = t,
                        prefixedText = norm_prefixed,
                        is_invalid_model = true,
                        content_model = UNSUPPORTED_CONTENT_MODELS[cm_low]
                    }
                    page_probe_cache[title_str] = info
                    page_probe_cache[half_key] = info
                    return info
                end

                local py_code, has_py_tag, other_lang = extract_python_code(raw_content)
                if other_lang then
                    local info = {
                        title = t,
                        prefixedText = norm_prefixed,
                        is_invalid_model = true,
                        content_model = other_lang
                    }
                    page_probe_cache[title_str] = info
                    page_probe_cache[half_key] = info
                    return info
                end

                if mw.ustring.gsub(py_code, "[\n\r%s]+", "") ~= "" then
                    -- 判斷是否具備明確的檔名+.py 後綴 (注意:條目 [[.py]] 的點號前無檔名,故 has_py_ext 為 false)
                    local has_py_ext = (norm_prefixed:match("[^/:]+%.py$") ~= nil)
                    local is_ambiguous = (not has_py_tag) and (not has_py_ext)
                    local is_not_py = is_ambiguous and is_obvious_non_python(py_code)

                    local info = {
                        title = t,
                        raw_prefixedText = t.prefixedText or cand_str,
                        prefixedText = norm_prefixed,
                        namespace = t.namespace or 0,
                        py_code = py_code,
                        has_py_tag = has_py_tag,
                        has_py_ext = has_py_ext,
                        is_ambiguous = is_ambiguous,
                        is_not_python = is_not_py
                    }
                    page_probe_cache[title_str] = info
                    page_probe_cache[half_key] = info
                    page_probe_cache[cand_str] = info
                    page_probe_cache[norm_prefixed] = info
                    return info
                end
            end
        end
    end

    page_probe_cache[title_str] = false
    page_probe_cache[half_key] = false
    return nil
end

-- 路徑處理工具
local function get_parent_dir(path_str)
    if not path_str or path_str == "" then return "" end
    local clean = path_str:gsub("%.py$", ""):gsub("/__init__$", ""):gsub("/+$", "")
    return clean:match("^(.*)/[^/]+$") or ""
end

local function join_wiki_path(base_dir, rel_path)
    local clean_rel = (rel_path or ""):gsub("^/+", "")
    if not base_dir or base_dir == "" or base_dir == "/" then
        return clean_rel
    end
    local clean_base = base_dir:gsub("/+$", "")
    if clean_rel == "" then return clean_base end
    return clean_base .. "/" .. clean_rel
end

local function pop_dir_levels(base_dir, count)
    local cur = (base_dir or ""):gsub("/+$", "")
    for _ = 1, count do
        local parent = cur:match("^(.*)/[^/]+$")
        if parent then
            cur = parent
        elseif cur ~= "" then
            cur = ""
        else
            return nil
        end
    end
    return cur
end

local function resolve_candidate_at(path_prefix, explicit_py)
    if not path_prefix or path_prefix == "" then return nil end
    local clean_prefix = path_prefix:gsub("%.py$", "")

    if not explicit_py then
        local init_py = probe_wiki_page(clean_prefix .. "/__init__.py")
            or probe_wiki_page(clean_prefix .. "/__init__")
        if init_py and not init_py.is_invalid_model and not init_py.is_not_python then
            return {
                kind = "package",
                page = init_py,
                mod_dir = clean_prefix,
                canonical_key = clean_prefix
            }
        end
    end

    local mod_page = probe_wiki_page(clean_prefix .. ".py")
        or probe_wiki_page(path_prefix)
        or probe_wiki_page(clean_prefix)
    if mod_page then
        return {
            kind = "module",
            page = mod_page,
            mod_dir = get_parent_dir(clean_prefix),
            pkg_dir = clean_prefix,
            canonical_key = clean_prefix
        }
    end

    return nil
end

local function get_or_create_namespace_pkg(mod_name, dir_path)
    local reg = SysRegistry.modules_registry
    if reg[dir_path] then
        reg[mod_name] = reg[dir_path]
        return reg[dir_path]
    end
    if reg[mod_name] then return reg[mod_name] end

    local PyClass = require("Module:PythonScript/Class")
    local module_class = Runtime.builtins.module_type or { __type = "class", __name = "module", __mro = {} }
    local pkg_obj = PyClass.make_instance(module_class)
    pkg_obj.__dict = {
        ["__name__"] = mod_name,
        ["__package__"] = mod_name,
        ["__dir__"] = dir_path,
        ["__path__"] = { __type = "list", elements = { dir_path } }
    }
    reg[mod_name] = pkg_obj
    reg[dir_path] = pkg_obj
    return pkg_obj
end

-- 兩階段註冊並執行 Python 腳本頁面 (含 Content Model 攔截與模糊地帶 SyntaxError 改寫)
local function load_and_exec_wiki_module(mod_name, cand_info, pkg_name)
    local page = cand_info.page
    local f_key = page.prefixedText

    -- 攔截不支援的 Content Model (如 javascript / css / json)
    if page.is_invalid_model then
        Runtime.raise("ImportError", "Page '" .. tostring(f_key) .. "' cannot be executed as Python (unsupported content model: " .. tostring(page.content_model) .. ")")
    end
    -- 攔截預檢確認非 Python 的頁面
    if page.is_not_python then
        Runtime.raise("ImportError", "Source page '" .. tostring(f_key) .. "' does not appear to be a valid Python script.")
    end

    local reg = SysRegistry.modules_registry
    local c_key = cand_info.canonical_key

    local existing = reg[c_key] or reg[f_key] or reg[mod_name]
    if existing then
        reg[mod_name] = existing
        reg[c_key] = existing
        return existing
    end

    local PyClass = require("Module:PythonScript/Class")
    local Lexer = require("Module:PythonScript/Lexer")
    local Parser = require("Module:PythonScript/Parser")
    local EvalUtils = require("Module:PythonScript/Evaluator/Utils")
    local _, _, _, exec_block = require("Module:PythonScript/Evaluator/Core").getCoreFunction()

    local mod_scope = EvalUtils.create_scope(nil)
    local vars = mod_scope.vars
    vars["__name__"] = mod_name
    vars["__file__"] = f_key
    vars["__dir__"] = cand_info.mod_dir
    vars["__package__"] = pkg_name or (cand_info.kind == "package" and mod_name or get_parent_dir(mod_name):gsub("/", "."))
    if cand_info.kind == "package" then
        vars["__path__"] = { __type = "list", elements = { cand_info.mod_dir } }
    elseif cand_info.pkg_dir then
        vars["__path__"] = { __type = "list", elements = { cand_info.pkg_dir } }
    end

    local module_class = Runtime.builtins.module_type or { __type = "class", __name = "module", __mro = {} }
    local loaded_obj = PyClass.make_instance(module_class)
    loaded_obj.__dict = vars

    reg[mod_name] = loaded_obj
    reg[c_key] = loaded_obj
    reg[f_key] = loaded_obj

    local py_code = page.py_code
    local new_file_idx = #Runtime.Traceback.files + 1
    Runtime.Traceback.files[new_file_idx] = { name = f_key, raw_code = py_code }

    local prev_file_idx = Runtime.Traceback.current_file_idx
    local prev_func_name = Runtime.Traceback.current_func_name or "<module>"
    local prev_line = Runtime.Traceback.current_line
    table.insert(Runtime.Traceback.stack, { prev_file_idx, prev_func_name, prev_line })

    Runtime.Traceback.current_file_idx = new_file_idx
    Runtime.Traceback.current_func_name = "<module>"
    Runtime.Traceback.current_line = 1

    -- 先執行詞法與語法解析,若為模糊地帶頁面且發生 SyntaxError / IndentationError,改寫為 ImportError
    local ok_parse, ast_or_err = pcall(function()
        local tokens = Lexer.tokenize(py_code)
        return Parser.parse(tokens, "module")
    end)

    if not ok_parse then
        table.remove(Runtime.Traceback.stack)
        Runtime.Traceback.current_file_idx = prev_file_idx
        Runtime.Traceback.current_func_name = prev_func_name
        Runtime.Traceback.current_line = prev_line
        reg[mod_name] = nil
        reg[c_key] = nil
        reg[f_key] = nil

        if page.is_ambiguous then
            Runtime.raise("ImportError", "Source page '" .. tostring(f_key) .. "' does not appear to be a valid Python script.")
        end
        error(ast_or_err, 0)
    end

    local ok_exec, err_or_sig = pcall(function()
        return exec_block(ast_or_err.body, mod_scope)
    end)

    table.remove(Runtime.Traceback.stack)
    Runtime.Traceback.current_file_idx = prev_file_idx
    Runtime.Traceback.current_func_name = prev_func_name
    Runtime.Traceback.current_line = prev_line

    if not ok_exec then
        reg[mod_name] = nil
        reg[c_key] = nil
        reg[f_key] = nil
        error(err_or_sig, 0)
    end

    return loaded_obj
end

-- 沿著基底目錄 base_dir 階層式載入 parts 陣列 (自動執行沿途的 __init__.py 並掛載子模組屬性)
local function load_chain_from_base(base_dir, parts, mod_name_prefix)
    local cur_dir = base_dir
    local parent_mod = nil
    local cur_mod_name = mod_name_prefix or ""

    for i = 1, #parts do
        local part = parts[i]
        cur_mod_name = (cur_mod_name ~= "") and (cur_mod_name .. "." .. part) or part
        local step_path = join_wiki_path(cur_dir, part)
        local is_last = (i == #parts)

        local cand = resolve_candidate_at(step_path, false)
        local step_mod
        if cand then
            local pkg_name = (cand.kind == "package") and cur_mod_name or (parent_mod and parent_mod.__dict["__name__"] or "")
            step_mod = load_and_exec_wiki_module(cur_mod_name, cand, pkg_name)
            cur_dir = (cand.kind == "package") and cand.mod_dir or (cand.pkg_dir or step_path)
        elseif not is_last then
            -- 中間層級無 __init__.py,建立隱式命名空間套件繼續深入子頁面
            step_mod = get_or_create_namespace_pkg(cur_mod_name, step_path)
            cur_dir = step_path
        else
            return nil
        end

        if parent_mod and parent_mod.__dict then
            parent_mod.__dict[part] = step_mod
        end
        parent_mod = step_mod
    end

    return parent_mod
end

function Import.execute_import(module_name, global_env)
    local loaded_obj
    -- 將匯入名稱中的全形「_」統一轉為半形「_」
    module_name = to_halfwidth_underscore(module_name)

    -- 若傳入的 global_env 為嵌套函式作用域,自動向上追溯至該模組的根作用域
    local root_env = global_env
    if type(root_env) == "table" and root_env.vars then
        while root_env.parent do root_env = root_env.parent end
    end
    local env_vars = (type(root_env) == "table" and root_env.vars) or root_env or {}

    -- 若沙盒環境將 __builtins__ 設為 None 或未包含 __import__ 的白名單字典,阻斷 import 語句
    local cb = env_vars["__builtins__"]
    if cb ~= nil then
        if cb == Runtime.builtins.None or (type(cb) == "table" and cb.__class and cb.__class.__name == "NoneType") then
            Runtime.raise("ImportError", "__import__ not found")
        elseif type(cb) == "table" and cb.__type == "dict" then
            local has_imp = false
            for _, p in ipairs(cb.pairs or {}) do
                if p.key == "__import__" and p.value ~= Runtime.builtins.None then
                    has_imp = true; break
                end
            end
            if not has_imp then
                Runtime.raise("ImportError", "__import__ not found")
            end
        end
    end

    local caller_name = env_vars["__name__"] or "__main__"
    local caller_dir = env_vars["__dir__"]

    if not caller_dir then
        if caller_name ~= "__main__" then
            local reg_caller = SysRegistry.modules_registry[caller_name]
            if reg_caller and reg_caller.__dict and reg_caller.__dict["__dir__"] then
                caller_dir = reg_caller.__dict["__dir__"]
            else
                caller_dir = get_parent_dir(caller_name:gsub("%.", "/"))
            end
        elseif SysRegistry.path_registry and SysRegistry.path_registry[1] then
            caller_dir = SysRegistry.path_registry[1]
        end
    end

    -- ==========================================
    -- 情況 1: 顯式相對匯入 (from . import utils / from ..pkg import mod)
    -- ==========================================
    if string.sub(module_name, 1, 1) == "." then
        local dots, rest = string.match(module_name, "^(%.+)(.*)$")
        if not caller_dir or (caller_dir == "" and caller_name == "__main__") then
            Runtime.raise("ImportError", "attempted relative import with no known parent package")
        end

        local target_base = pop_dir_levels(caller_dir, #dots - 1)
        if not target_base then
            Runtime.raise("ImportError", "attempted relative import beyond top-level package")
        end

        -- 若單獨請求 "." 或 ".." (例如 Stmt.lua 在 from . import utils 第一步探測 ".")
        if rest == "" then
            local pkg_cand = resolve_candidate_at(target_base, false)
            if pkg_cand and pkg_cand.kind == "package" then
                local pkg_name = target_base:match("([^/:]+)$") or target_base
                return load_and_exec_wiki_module(pkg_name, pkg_cand, pkg_name)
            end
            return get_or_create_namespace_pkg(target_base, target_base)
        end

        local parts = {}
        for part in string.gmatch(rest:gsub("%.", "/"), "[^/]+") do
            table.insert(parts, part)
        end
        local base_pkg_name = target_base:match("([^/:]+)$") or ""
        local rel_mod = load_chain_from_base(target_base, parts, base_pkg_name)
        if rel_mod then
            SysRegistry.modules_registry[module_name] = rel_mod
            return rel_mod
        end
        Runtime.raise("ImportError", "cannot import name '" .. rest .. "' from '" .. (target_base ~= "" and target_base or ".") .. "'")
    end

    -- ==========================================
    -- 情況 2: 系統內建模組與 Python 標準庫攔截
    -- ==========================================
    if module_name == "mw" then
        if not SysRegistry.modules_registry["mw"] then
            SysRegistry.modules_registry["mw"] = require("Module:PythonScript/Mw").get_module()
        end
        return SysRegistry.modules_registry["mw"]
    end

    local Library = require("Module:PythonScript/Library")
    if Library.has_module(module_name) then
        if not SysRegistry.modules_registry[module_name] then
            SysRegistry.modules_registry[module_name] = Library.get_module(module_name, env_vars)
        end
        return SysRegistry.modules_registry[module_name]
    end

    if SysRegistry.modules_registry[module_name] then
        return SysRegistry.modules_registry[module_name]
    end

    -- ==========================================
    -- 情況 3: 嘗試作為原生 Lua 模組 (Namespace 828) 或維基模板 (Namespace 10) 載入
    -- ==========================================
    local is_explicit_py = (module_name:sub(-3):lower() == ".py")
    if not is_explicit_py then
        local success, title_checker = pcall(mw.title.new, module_name)
        if success and title_checker and title_checker.namespace == 828 then
            local ok_lua, lua_mod = pcall(require, module_name)
            if ok_lua and type(lua_mod) == "table" then
                local MwAPI = require("Module:PythonScript/Mw")
                loaded_obj = {}
                for k, v in pairs(lua_mod) do loaded_obj[k] = v end
                loaded_obj = MwAPI.make_lazy_namespace(module_name, loaded_obj, true)
                SysRegistry.modules_registry[module_name] = loaded_obj
                return loaded_obj
            end
        end

        if success and title_checker and title_checker.namespace == 10 then
            local template_callable = {
                __type = "function",
                __name = module_name,
                __qualname__ = module_name,
                __module__ = (title_checker and title_checker.prefixedText) or "Template",
                __call = function(py_args, py_kwargs)
                    local mw_args = {}
                    if py_args then
                        for i, arg in ipairs(py_args) do
                            local success_str, str_val = pcall(Runtime.builtins.str, {arg})
                            mw_args[i] = success_str and str_val or tostring(arg)
                        end
                    end
                    if py_kwargs then
                        for k, v in pairs(py_kwargs) do
                            local success_str, str_val = pcall(Runtime.builtins.str, {v})
                            mw_args[tostring(k)] = success_str and str_val or tostring(v)
                        end
                    end
                    local frame = mw.getCurrentFrame()
                    local success_expand, result = pcall(function()
                        return frame:expandTemplate{ title = module_name, args = mw_args }
                    end)
                    if not success_expand then
                        Runtime.raise("RuntimeError", "Template expansion failed for '" .. module_name .. "': " .. tostring(result))
                    end
                    return result
                end
            }
            SysRegistry.modules_registry[module_name] = template_callable
            return template_callable
        end
    end

    -- ==========================================
    -- 情況 4: 階層式搜尋並載入維基 Python 套件與腳本 (支援 __init__.py 與 sys.path)
    -- ==========================================
    -- 建構搜尋根目錄清單:1. 呼叫者所在目錄 (__dir__) -> 2. sys.path -> 3. 維基全域根目錄 ("")
    local search_roots = {}
    local seen_roots = {}
    local function add_root(r)
        if type(r) == "string" and not seen_roots[r] then
            seen_roots[r] = true
            table.insert(search_roots, r)
        end
    end
    if caller_dir and caller_dir ~= "" then add_root(caller_dir) end
    for _, p in ipairs(SysRegistry.path_registry or {}) do
        if type(p) == "string" then add_root(p) end
    end
    add_root("")

    -- [4A] 若傳入完整維基頁面路徑 (含 ':' 或 '/' 或 '.py',如 "Module:Foo/somefile.py")
    if module_name:find(":", 1, true) or module_name:find("/", 1, true) or is_explicit_py then
        local clean_mod = module_name:gsub("%.py$", "")
        for _, root in ipairs(search_roots) do
            local full_p = join_wiki_path(root, clean_mod)
            local cand = resolve_candidate_at(full_p, is_explicit_py)
            if cand then
                -- 若父目錄存在 __init__.py 且尚未執行,先初始化父套件
                local p_dir = cand.mod_dir
                if p_dir and p_dir ~= "" and not SysRegistry.modules_registry[p_dir] then
                    local init_cand = resolve_candidate_at(p_dir, false)
                    if init_cand and init_cand.kind == "package" then
                        local p_name = p_dir:match("([^/:]+)$") or p_dir
                        load_and_exec_wiki_module(p_name, init_cand, p_name)
                    end
                end
                local mod_obj = load_and_exec_wiki_module(module_name, cand, p_dir)
                SysRegistry.modules_registry[module_name] = mod_obj
                return mod_obj
            end
        end
    end

    -- [4B] 標準點號路徑拆解 (如 "腳本"、"腳本.腳本1"、"utils")
    local parts = {}
    for part in string.gmatch(module_name:gsub("%.py$", ""), "[^%.]+") do
        table.insert(parts, part)
    end

    if #parts > 0 then
        local rel_joined = table.concat(parts, "/")
        for _, root in ipairs(search_roots) do
            local full_target = join_wiki_path(root, rel_joined)
            -- 確認最終目標 (腳本1.py 或 腳本/__init__.py) 確實存在於此 root 下,才沿途啟動階層載入
            local leaf_cand = resolve_candidate_at(full_target, false)
            if leaf_cand then
                local loaded = load_chain_from_base(root, parts, "")
                if loaded then
                    SysRegistry.modules_registry[module_name] = loaded
                    -- 若為多層級 import A.B,同步確保頂層套件 A 可在作用域中存取
                    local top_name = parts[1]
                    if #parts > 1 and SysRegistry.modules_registry[top_name] and env_vars[top_name] == nil then
                        env_vars[top_name] = SysRegistry.modules_registry[top_name]
                    end
                    return loaded
                end
            end
        end
    end

    Runtime.raise("ImportError", "No module named '" .. module_name .. "'")
end

Import.probe_wiki_page = probe_wiki_page
Import.to_halfwidth_underscore = to_halfwidth_underscore

return Import