---
author: malys
description: List of reusable functions.
pageDecoration.prefix: "🛠️ "
name: "Library/Malys/Utilities"
tags: meta/library
share.uri: "https://github.com/malys/silverbullet-libraries/blob/main/src/Utilities.md"
share.hash: 3ccf4b64
share.mode: pull
---
# Utilities

*   `mls.getmeetingTitle()`: Extracts meeting title from page URL.
*   `mls.embedUrl(url, w, h)`: Embeds a URL using an iframe (width & height optional).
*   `mls.debug(message, prefix)`: Prints debug message (if `LOG_ENABLE` is true).
*   `mls.getCodeBlock(page, blockId, token, text)`: Retrieves code from a Markdown code block by ID, with optional token replacement.
*   `mls.parseISODate(isoDate)`: Parses an ISO date string to a timestamp (potential error with `xoffset`).
*   `mls.getStdlibInternal()`: Gets a list of internal API keys (cached).
*   `mls.positionToLineColumn(text, pos)`: Converts a text position to line/column numbers.
*   `table.appendArray(a, b)`: Appends array `b` to array `a`.
*   `table.unique(array)`: Returns unique elements from an array.
*   `table.uniqueKVBy(array, keyFn)`: Returns unique key-value pairs based on a key function.
*   `table.mergeKV(t1, t2)`: Merges two tables (recursive for nested tables).
*   `table.map(t, fn)`: Applies a function to each element of an array.
*   `table.toMd(data)`: Converts a table (or JSON) to a Markdown table.
*   `embed.md(url)`: Embed external md
*   `embed.page(url,width,height)`: Embed page in a iframe

## Debug

### How to enable debug mode

* Create space-lua with:
```lua
LOG_ENABLE=true
```

* Reload system.
* Open Chrome Console
* Add filter “[Client] [DEBUG]“
## Code
```space-lua
-- priority: 20
mls=mls or {}

-- Convert meeting note title
-- Extracts the meeting title from the current page URL.
-- Assumes the URL format is like "namespace/Meeting_Note Title".
-- Splits the URL by "/", takes the last part, and then splits that by "_"
-- to remove the "Meeting" prefix and joins the remaining parts with a space.
function mls.getmeetingTitle()
  local t=string.split(string.split(editor.getCurrentPage(),"/")[#string.split(editor.getCurrentPage(),"/")],"_")
  table.remove(t,1)
  t=table.concat(t, " ")
  return t
end

-- Embed external resources
-- Creates an HTML iframe to embed an external URL within the current page.
-- Allows specifying width and height, defaulting to "100%" and "400px" respectively.
function mls.embedUrl(specOrUrl,w,h) 
  local width = w or "100%"
  local height = h or "400px"
  return widget.html(dom.iframe {
    src=specOrUrl,
    style="width: " .. width .. "; height: " .. height
  })
end

---------------------------------------------
---- Debug  ---
---------------------------------------------
-- Pretty-print any Lua value (tables included)
-- Recursively prints a Lua value, handling tables with indentation.
-- Prevents infinite recursion by limiting the depth to 5.
local function dump(value, depth)
  --print("[DEBUG][TYPE]"..type(value)) -- commented out debug line
  depth = depth or 0
  if type(value) ~= "table" or (type(value) == "string" and value ~= "[object Object]") then
    return value
  end

  -- Prevent going too deep (avoid infinite recursion)
  if depth > 5 then
    return "{ ... }"
  end

  local indent = string.rep("  ", depth)
  local next_indent = string.rep("  ", depth + 1)

  local parts = {}
  table.insert(parts, "{")

  for k, v in pairs(value) do
    local key = tostring(k)
    local val
    if type(v) == "table" then
      val = dump(v, depth + 1)
    else
      val = v
    end

    table.insert(parts, next_indent .. tostring(key) .. " = " .. tostring(val) .. ",")
  end

  table.insert(parts, indent .. "}")

  return table.concat(parts, "\n")
end


-- Debug function
-- Prints a debug message to the console if LOG_ENABLE is true.
-- Uses the dump function to pretty-print the message.
-- Allows specifying a prefix for the debug message.
function mls.debug(message, prefix)
  if not LOG_ENABLE then
    return message
  end
  local log_message = dump(message)

  local result = "[DEBUG]"
  if prefix then
    result = result .. "[" .. tostring(prefix) .. "]"
  end

  result = result .. " " .. tostring(log_message)
  print(result)
  return result
end


---------------------------------------------
---- Code Block code extraction ---
---------------------------------------------
-- Get child of node
-- Helper function to find a child node of a given type within a parent node.
local getChild = function(node, type)
  for _, child in ipairs(node.children) do
    if child.type == type then
      return child
    end
  end
end

-- Get text of Node
-- Helper function to recursively extract the text content from a node.
local getText = function(node)
  if not node then
    return nil
  end
  if node.text then
    return node.text
  else
    for _, child in ipairs(node.children) do
      local text = getText(child)
      if text then
        return text
      end
    end
  end
end

-- Find codeblock
-- Recursively searches for a code block (FencedCode node) with a specific blockId in the node's children.
local findMyFence = function(node,blockId)
    if not node.children then
      return nil
    end
    for _, child in ipairs(node.children) do
        --mls.debug(child) -- commented out debug line
        if child.type == "FencedCode" then
          local info = getText(getChild(child, "CodeInfo"))
          --mls.debug(info) -- commented out debug line
          local block= string.gsub(blockId, "[^%w]", "")
          local inf=string.gsub(info, "[^%w]", "")
          if info and info:find(blockId, 1, true) then
            mls.debug(info)
            return getChild(child, "CodeText")
          end
        end
        local result= findMyFence(child,blockId)
        if result ~=nil then
          return result
        end
      --break --  for loop
    end --for
end

-- Get code source in md codeblock
-- Parses a Markdown page, finds a code block with the given blockId, and returns its content.
-- Allows replacing a token within the code block with a new text value.
function mls.getCodeBlock  (page,blockId,token,text)
  local tree = markdown.parseMarkdown(space.readPage(page))
  --mls.debug(tree) -- commented out debug line
  if tree then
    local fence = findMyFence(tree,blockId)
    --mls.debug(fence) -- commented out debug line
    if fence then
      local result=fence.children[1].text
      if token == nil or text==nil then
        return result
      else
        return string.gsub(result, token, text)
      end
    end
  end
  return "Error"
end

-- Parse ISO Date
-- Parses an ISO date string into a Unix timestamp.
function mls.parseISODate(isoDate)
    local pattern = "(%d+)%-(%d+)%-(%d+)%a(%d+)%:%d+:%d+([Z%+%-])(%d?%d?)%:?(%d?%d?)"
    local year, month, day, hour, minute, 
        seconds, offsetsign, offsethour, offsetmin = string.match(isoDate, pattern)
    local timestamp = os.time{year = year, month = month, 
        day = day, hour = hour, min = minute, sec = seconds}
    local offset = 0
    if offsetsign ~= 'Z' then
      offset = tonumber(offsethour) * 60 + tonumber(offsetmin)
      if offsetsign == "-" then offset = offset * -1 end
    end
    
    return timestamp + offset
end

-- Get Stdlib Internal
-- Retrieves a list of internal API keys from a remote file.
-- Caches the results to avoid repeated fetching.
-- The URL points to a file in the silverbulletmd/silverbullet repository.
function mls.getStdlibInternal()
  if _G ~=nil then
    return table.keys(_G)
  end 
  -- get list of internal api TODO: remove
  local KEY="stdlib_internal"
  local result=mls.cache.ttl.CacheManager.get(KEY)
  if  result == nil then 
    local url = "https://raw.githubusercontent.com/silverbulletmd/silverbullet/refs/heads/main/client/space_lua/stdlib.ts"  
    local resp = net.proxyFetch(url)   
    if resp.status ~= 200 then  
      error("Failed to fetch file: " .. resp.status)  
    end  
    local content = resp.body  
    local results = {}   
    for line in string.split(content,"\n") do  
      local key = string.match(string.trim(line), 'env%.set%("(.*)"')  
      if key  then  
        table.insert(results, key)  
      end
    end  
    table.sort(results)
    result = results
    mls.cache.ttl.CacheManager.set(KEY,result)
  end  
  return result
end

-- Position to Line Column
-- Converts a character position within a text string to a line and column number.
function mls.positionToLineColumn(text, pos)  
  local line = 1  
  local column = 0  
  local lastNewline = -1  
    
  for i = 0, pos - 1 do  
    if string.sub(text, i + 1, i + 1) == "\n" then  
      line = line + 1  
      lastNewline = i  
      column = 0  
    else  
      column = column + 1  
    end  
  end  
    
  return { line = line, column = column }  
end

-----------------------------------------
-- TABLE
-----------------------------------------
-- Append Array
-- Appends all elements from one array to another.
function table.appendArray(a, b)
    if a~= nil and b ~= nil then
      for i = 1, #b do
          table.insert(a, b[i])
      end
    end
    return a
end

-- Unique
-- Returns a new array containing only the unique elements from the input array.
function table.unique(array)
    local seen = {}
    local result = {}

    for i = 1, #array do
        local v = array[i]
        if not seen[v] then
            seen[v] = true
            result[#result + 1] = v
        end
    end

    return result
end

-- Unique KV By
-- Returns a new array containing only the unique key-value pairs from the input array,
-- based on a provided key function.
function table.uniqueKVBy(array, keyFn)
    local seen = {}
    local result = {}

    for i = 1, #array do
        local key = keyFn(array[i])
        if not seen[key] then
            seen[key] = true
            result[#result + 1] = array[i]
        end
    end

    return result
end

-- Merge KV
-- Merges two tables, recursively merging nested tables.
-- If a key exists in both tables and the values are both tables, the nested tables are merged.
-- Otherwise, the value from the second table overwrites the value from the first table.
function  table.mergeKV(t1, t2)
    for k, v in pairs(t2) do
        if type(v) == "table" and type(t1[k]) == "table"  then
            table.mergeKV(t1[k], v)
        else
            t1[k] = v
        end
    end
    return t1
end

-- Map
-- Applies a function to each element of an array and returns a new array with the results.
function table.map(t, fn)
  local result = {}
  for i, v in ipairs(t) do
    result[i] = fn(v, i)
  end
  return result
end

-- To Md
-- Converts a Lua table to a Markdown table string.
-- Handles both arrays of arrays and arrays of objects.
-- Can parse JSON or Lua strings as input.
function table.toMd(data)
    local tbl
    local input=string.trim(data)
    -- If input is a string, try parsing as JSON
    if string.startsWith(data,"{") then
      -- Lua
      tbl=spacelua.evalExpression(spacelua.parseExpression(data)) 
    else
      --JSON
      tbl=js.tolua(js.window.JSON.parse(js.tojs(data)))
    end  
    if #tbl == 0 then return "" end

    local md = {}

    -- Helper to convert a row to Markdown
    local function rowToMd(row)
        local cells = {}
        for _, cell in ipairs(row) do
            table.insert(cells, tostring(cell))
        end
        return "| " .. table.concat(cells, " | ") .. " |"
    end

    -- Handle array of objects
    local first = tbl[1]
    local headers
    if type(first) == "table" and not (#first > 0) then
        headers = {}
        for k in pairs(first) do table.insert(headers, k) end
        local rows = {headers}
        for _, obj in ipairs(tbl) do
            local row = {}
            for _, h in ipairs(headers) do
                table.insert(row, obj[h] or "")
            end
            table.insert(rows, row)
        end
        tbl = rows
    end

    -- Header
    table.insert(md, rowToMd(tbl[1]))

    -- Separator
    local sep = {}
    for _ = 1, #tbl[1] do table.insert(sep, "---") end
    table.insert(md, "| " .. table.concat(sep, " | ") .. " |")

    -- Data rows
    for i = 2, #tbl do
        table.insert(md, rowToMd(tbl[i]))
    end

    return table.concat(md, "\n")
end
---- Embed
function embed.page(src,width,height)
  local w=width or "100%"
  local h=height or "400px"
  return widget.html(dom.iframe {  
    src = src,  
    style = "width: "..w.."; height: "..h.."; border: 0;",  
    sandbox = "allow-same-origin allow-scripts",  
  })
end

function embed.md(url)
  local result = http.request(url)
  local tree = markdown.parseMarkdown(result.body)
  local rendered = markdown.renderParseTree(tree)
  return widget.new { markdown = rendered:gsub("<[^>]*>","") }
end

mls.ui = mls.ui or {}

function mls.ui.filterBoxMulti(title, items, hint)
  hint = hint or "Cancel/Esc when done"
  local selected = {}
  local remaining = {}

  for _, item in ipairs(items) do
    table.insert(remaining, item)
  end

  while #remaining > 0 do
    local chosen = editor.filterBox(title, remaining, hint)
    if not chosen then break end

    table.insert(selected, chosen)

    local new_remaining = {}
    for _, item in ipairs(remaining) do
      if item ~= chosen then
        table.insert(new_remaining, item)
      end
    end
    remaining = new_remaining
  end

  -- extract the string to return: try common field names, fallback to tostring
  local results = {}
  for _, item in ipairs(selected) do
    if type(item) == "string" then
      table.insert(results, item)
    else
      table.insert(results, item.name or item.label or item.value or item.text or tostring(item))
    end
  end

  return table.concat(results, ", ")
end
```

## Changelog

* 2026-05-29:
  * fix: `parseISODate` referenced undefined `json_date`/`xoffset` (now `isoDate`/`offsetsign`)
  * fix: `getStdlibInternal` used `table.sort`'s return value (always `nil`); now sorts in place and returns the list
  * fix: `table.mergeKV` called undefined `mergeTablesRecursive`; now recurses into `table.mergeKV`
* 2026-02-25:
  * feat: embed md and page 
* 2026-01-22:
  * getStdlibInternal compatible with edge version https://community.silverbullet.md/t/risk-audit/3562/14
* 2026-01-20:
  * feat: add table functions
    * map
    * mergeKV
    * uniqueKVBy
    * unique
    * appendArray
  * feat: add `mls.getStdlibInternal` function
