Module:Size

From Star of Providence Wiki
Jump to navigation Jump to search

Details

The Size module offers a Lua class for parsing sizes in the format used for image links. It's intended to be used by other Lua modules instead of templates.

It has no dependencies and exports a Size "class" you can use directly. It provides annotations documenting the various functions that are understood by Lua Language Server.


--- Module:Size
--- by User:LtDk, licensed CC-BY-SA 4.0
--- Simple Lua class for parsing image sizes.

local Size = {}
local SizeKeys = {
    width  = true,
    height = true,
}

--- WWpx, xHHpx, and WWxHHpx sizes.
--- @class Size
--- @field width  number?
--- @field height number?
local SizeProto = {}
local SizeMeta = {}

--- Checks if a value is a Size.
--- @param value any
--- @return boolean
function Size.isSize(value)
    return type(value) == 'table' and rawequal(getmetatable(value), SizeMeta)
end

function SizeMeta.__eq(lhs, rhs)
    if not Size.isSize(rhs) then
        return false
    end
    return lhs.width == rhs.width and lhs.height == rhs.height
end

--- Sizes can have nil width or nil height, but not nil width and nil height.
function SizeMeta:__newindex(k, v)
    if not SizeKeys[k] then
        error(string.format('invalid key %s', k))
    end
    if v ~= nil and (type(v) ~= 'number' or math.floor(v) ~= v or v <= 0) then
        error(string.format('%s must be positive integer or nil, was %s', k, v))
    end

    if v == nil then
        if k == 'width' and self.height == nil then
            error('cannot set width to nil when height is already nil')
        elseif k == 'height' and self.width == nil then
            error('cannot set height to nil when width is already nil')
        end
    end

    rawset(self, '__' .. k, v)
end

--- We rename the actual keys to ensure newindex always fires.
function SizeMeta:__index(k)
    if not SizeKeys[k] then
        return SizeProto[k]
    else
        return rawget(self, '__' .. k)
    end
end

--- Sizes are formatted as WWpx, xHHpx, or WWxHHpx.
function SizeMeta:__tostring()
    local w = ''
    local h = ''
    if self.height ~= nil then
        h = 'x' .. tostring(self.height)
    end
    if self.width ~= nil then
        w = tostring(self.width)
    end
    return w .. h .. 'px'
end

--- Parses a size from a string. Accepts WWpx, xHHpx, and WWxHHpx.
--- @param size string
--- @return Size
function Size.parse(size)
    size = tostring(size)

    local parsed = Size.tryParse(size)

    if parsed == nil then
        error(string.format('%q is not a valid size', size))
    end

    return parsed
end

--- Attempts to parse a Size from a string, returning nil instead of errors.
--- @param size string
--- @return Size?
function Size.tryParse(size)
    size = mw.text.trim(tostring(size)) --[[@as string]]

    if string.sub(size, -2) == 'px' then
        size = mw.text.trim(string.sub(size, 1, -3)) --[[@as string]]
    end

    local width = ''
    local height = ''

    local i, j = string.find(size, 'x', 1, true)
    if i == nil then
        width = size
    else
        width = mw.text.trim(string.sub(size, 1, i - 1)) --[[@as string]]
        height = mw.text.trim(string.sub(size, j + 1)) --[[@as string]]
    end

    local width_n = tonumber(width, 10)
    local height_n = tonumber(height, 10)

    if width_n == nil and (width ~= '' or height_n == nil) then
        return nil
    end
    if height_n == nil and (height ~= '' or width_n == nil) then
        return nil
    end

    if width_n ~= nil and (math.floor(width_n) ~= width_n or width_n < 0) then
        return nil
    end
    if height_n ~= nil and (math.floor(height_n) ~= height_n or height_n < 0) then
        return nil
    end

    return Size.new(width_n, height_n)
end

--- Creates a new Size from parts.
--- @param width  number?
--- @param height number?
--- @return Size
function Size.new(width, height)
    local size = setmetatable({ width = width, height = height }, SizeMeta)

    if width == nil and height == nil then
        error('Size cannot have both nil width and nil height')
    end

    if width ~= nil and (type(width) ~= 'number' or math.floor(width) ~= width or width < 0) then
        error(string.format('width must be a positive integer or nil, was %s', width))
    end
    if height ~= nil and (type(height) ~= 'number' or math.floor(height) ~= height or height < 0) then
        error(string.format('height must be a positive integer or nil, was %s', height))
    end

    return size
end

return Size