Модуль:Regex

Regex
Описание:
Выполняет поиск или замену с помощью регулярного выражения
Автор:
Александр Машин
Параметры:
1:
Строка для поиска или замены
flavour:
разновидность регулярного выражения (pcre2, posix, и т.д.)
subpattern:
номер захвата, который должен возвратить успешный поиск
sep:
разделитель между найденными подстроками
template:
шаблон, которым должны быть обёрнуты найденные значения
intro:
строка, вставляемая перед найденными значеняими
outro:
строка, подставляемая после найденных значений
default:
значение по умолчанию, возвращаемое вместо найденных значений
limit:
максимальное количество возвращённых совпадений
Возвращает:
Найденные значения или строка, в которой произведены замены
Синтаксис:
{{#invoke:Regex|Regex
    | Строка для поиска или замены

    | flavour = разновидность регулярного выражения (pcre2, posix, и т.д.)

    | subpattern = номер захвата, который должен возвратить успешный поиск

    | sep = разделитель между найденными подстроками

    | template = шаблон, которым должны быть обёрнуты найденные значения

    | intro = строка, вставляемая перед найденными значеняими

    | outro = строка, подставляемая после найденных значений

    | default = значение по умолчанию, возвращаемое вместо найденных значений

    | limit = максимальное количество возвращённых совпадений

}}

Функция, производящая поиск по регулярному выражению в переданной строке, или поик и замену, в том числе, последовательную.

Знак | в регулярном выражении надо заменить на {{!}}. Знак = надо заменить на {{=}} или использовать синтаксис 2 = /regex/.

В строке замены захваты начинаются с %.

Примеры

Викитекст Результат
Поиск
{{#invoke:Regex|regex|а,б,в,г|%[аб]%u|sep=,}} String "%[аб]%u" is not expected here.
{{#invoke:Regex|regex|one, two, three|%\w+%|template=lang-en|sep=, <nowiki />}} англ. one, англ. two, англ. three
{{#invoke:Regex|regex|а,б,в,г|%(?:^{{!}},)(.*?)(?:,{{!}}$)%u}} ,)(.*?)(?:,|$)%u" is not expected here.
Замена
{{#invoke:Regex|regex|а,б,в,г|%[аб]%u|[[%0]]|limit=1}} String "%[аб]%u" is not expected here.
{{#invoke:Regex|regex|а,б,в,г|%[аб]%u|[[%0]]}} String "%[аб]%u" is not expected here.
{{#invoke:Regex|regex|а,б,в,г|%(?:^{{!}},)(.*?)(?:,{{!}}$)%|[[%1]]}} абвг
Многострочный синтаксис
{{#invoke:Regex|regex|Александр, Константин, Николай, Михаил
     | /(
              Александр
        {{!}} Николай
       )/x = царь
     | /Михаил/ = великий князь
     | /Константин/ = цесаревич
}}
царь, цесаревич, царь, великий князь



local concat = table.concat
local wrap, yield = coroutine.wrap, coroutine.yield

local function ordered_pairs (tbl)
	return wrap (function ()
		for key, value in ipairs (tbl) do
			yield (key, value)
		end
		for key, value in pairs (tbl) do
			if type (key) ~= 'number' then
				yield (key, value)
			end
		end
	end)
end
	
local function shallow (tbl)
	local cloned = {}
	for key, value in ordered_pairs (tbl) do
		cloned [key] = value
	end
	return cloned
end

local flavours = {}
for _, flavour in ipairs { 'posix', 'gnu', 'pcre', 'pcre2', 'onig', 'tre' } do
	flavours [flavour] = _G ['rex_' .. flavour]
end
flavours.pcre = flavours.pcre or flavours.pcre2 
flavours.pcre2 = flavours.pcre or flavours.pcre2
local pcre = flavours.pcre.new

local param_names = {
	'flavour',
	'subpattern', 'sep', 'template',
	'intro', 'outro', 'default', 'limit'
}

local flag_letters = {
	a = 'ANCHORED',
	i = 'CASELESS',
	m = 'MULTILINE',
	n = 'NO_AUTO_CAPTURE',
	s = 'DOTALL',
	u = 'UTF8',
	x = 'EXTENDED',
	A = 'ANCHORED',
	D = 'DOLLAR_ENDONLY',
	U = 'UNGREEDY',
	X = 'EXTRA'
}

local function allowed_flags (flavour)
	local constants = flavour.flags()
	local flags = {}
	for flag, const in pairs (flag_letters) do
		if constants [const] then
			flags [#flags + 1] = flag
		end
	end
	return concat (flags)
end

local function flags2options (consts, flags)
	-- To handle UTF8 correctly:
	if consts.UTF8 and consts.UCP then
		consts.UTF8 = consts.UTF8 + consts.UCP
	end
	local options = 0
	for flag in flags:gmatch '.' do
		options = options + consts [flag_letters  [flag]]
	end
	return options
end
		
local function regex_pattern (flavour)
	return pcre (([==[
		^
		(?<delim>[\W\\\S])
		(?<search>.*?)
		(?:
			\k<delim>
			(?<replace>.*)
		)?
		\k<delim> \s*
		(?<flags>[%s]*)
		$
	]==]):format (allowed_flags (flavour)), 'sx')
end

local function regex (args, expander)
	local subject

	local params = {}
	for _, param in ipairs (param_names) do
		params [param] = args [param]
		args [param] = nil
	end
	
	local flavour = flavours [params [flavour]] or flavours.pcre
	local new, gsub, tfind = flavour.new, flavour.gsub, flavour.tfind
	local regex_pattern = regex_pattern (flavour)
			
	local subs = {}
	for left, right in ordered_pairs (args) do
		if left == 1 then
			subject = right
		else
			local search, replace, flags
			if type (left) == 'number' then
				-- /search/replace/, or /search/, or /search/, replace syntax:
				local start, _, captures = regex_pattern:tfind (right)
				if start then
					search, replace, opts = captures.search, captures.replace, flags2options (flavour.flags(), captures.flags)
				else
					-- Could be /search/, replace syntax at replace:
					replace = right
				end
			else
				-- /search/ = replace syntax:
				local start, _, captures = regex_pattern:tfind (left)
				if start then
					search, replace, opts = captures.search, right, flags2options (flavour.flags(), captures.flags)
				end
			end
	
			if search then
				local regex = new (search, opts)
				if not regex then
					return '<span class="error">Regular expression /'
						.. search
						.. '/ does not compile</span>'
				end
				subs [#subs + 1] = { search = regex, replace = replace }
			else
				if subs [#subs] and subs [#subs].search and not subs [#subs].replace then
					subs [#subs].replace = replace
				else
					return '<span class="error">String "'
						.. ( replace or '' )
						.. '" is not expected here.</span>'
				end
			end
		end
	end
	
	local found = false
	for _, subst in ipairs (subs) do
		if subst.replace then
			subject = gsub (subject, subst.search, subst.replace, params.limit)
		else
			local matches, start = {}, 1
			while start and (not params.limit or #matches < params.limit) do
				local captures, finish = {}, nil
				start, finish, captures = subst.search:tfind (subject, start)
				if start then
					if params.subpattern then
						matches [#matches + 1] = captures [params.subpattern]
					else
						matches [#matches + 1] = subject:sub (start, finish)
					end
					captures [0] = matches [#matches]
					if params.template then
						if not captures [1] then
							captures [1] = captures [0]
						end
						matches [#matches] = expander (params.template, captures)
					end
					start = finish + 1
					found = true
				end
			end
			subject = (found and params.intro or '')
				.. (#matches > 0 and concat (matches, params.sep or '') or params.default or '')
				.. (found and params.outro or '')
		end
	end

	return subject
end

return {
	regex = function (tbl)
		local expander = tbl.expandTemplate and function (template, args)
			return tbl:expandTemplate { title = template, args = args }
		end or function (template, args)
			return '{{' .. template .. '|' .. concat (args, '|') .. '}}' -- @todo.
		end
		return regex (tbl.args and shallow (tbl.args) or tbl, expander)
	end
}