-- lx-bundle:true

--- Shared backslash escapes for rendered Markdown
-- @module plugins.mdpreview.escapes

local escapes = {}

-- CommonMark permits a backslash before any ASCII punctuation character
local ESCAPABLE_CHARACTERS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
local escapable = {}

for position = 1, #ESCAPABLE_CHARACTERS do
	escapable[ESCAPABLE_CHARACTERS:sub(position, position)] = true
end

--- Return whether one character can follow a Markdown escape
-- @param character string one source byte
-- @return boolean escapable_character true for supported ASCII punctuation
function escapes.is_escapable(character)
	return escapable[character] == true
end

--- Read one Markdown escape at a source position
-- @param text string Markdown source
-- @param position number possible backslash byte
-- @return string|nil character decoded punctuation
-- @return number|nil next_position first byte after the escape
function escapes.read(text, position)
	if text:sub(position, position) ~= "\\" then
		return nil
	end

	local character = text:sub(position + 1, position + 1)

	if not escapes.is_escapable(character) then
		return nil
	end

	return character, position + 2
end

--- Return whether a source byte follows an odd run of backslashes
-- @param text string Markdown source
-- @param position number source byte to inspect
-- @return boolean escaped true when the byte is escaped
function escapes.is_escaped(text, position)
	local backslash_count = 0
	local scan_position = position - 1

	while scan_position >= 1 and text:sub(scan_position, scan_position) == "\\" do
		backslash_count = backslash_count + 1
		scan_position = scan_position - 1
	end

	return backslash_count % 2 == 1
end

--- Find one literal marker that is not escaped
-- @param text string Markdown source
-- @param marker string literal marker
-- @param start_position number first search byte
-- @return number|nil position marker start
function escapes.find_unescaped(text, marker, start_position)
	local position = start_position

	while position <= #text do
		local found = text:find(marker, position, true)

		if not found then
			return nil
		end
		if not escapes.is_escaped(text, found) then
			return found
		end

		position = found + #marker
	end

	return nil
end

--- Decode recognized Markdown punctuation escapes
-- Backslashes before ordinary letters or other text remain visible
-- @param text string Markdown source
-- @return string decoded visible text
function escapes.decode(text)
	local parts = {}
	local position = 1

	while position <= #text do
		local source_character = text:sub(position, position)
		local character
		local next_position

		if source_character == "\\" then
			character, next_position = escapes.read(text, position)
		end

		if character and next_position then
			parts[#parts + 1] = character
			position = next_position
		else
			parts[#parts + 1] = source_character
			position = position + 1
		end
	end

	return table.concat(parts)
end

return escapes
