---
author: malys
description: Automatically formats Markdown table cells based on hashtag column tags.
tags: userscript meta
pageDecoration.prefix: "👁️ "
name: "Library/Malys/MdTableRender"
share.uri: "https://github.com/malys/silverbullet-libraries/blob/main/src/MdTableRender.md"
share.hash: 2a84ea90
share.mode: pull
---
# Md table column rendering

This script enhances Markdown tables inside SilverBullet by applying dynamic formatting rules to columns marked with hashtag-style format tags (e.g. `#euro`,
`#percent`, `#stars`). It observes table changes in real time and transforms raw text values into styled, formatted elements — such as currency, percentages,
booleans, dates, badges, emojis, trends, and star ratings — without altering the original Markdown source. It is designed to be non-intrusive, editable-friendly, and resilient thanks to mutation observers, debouncing, and a polling fallback.

## Disclaimer & Contributions

This code is provided **as-is**, **without any kind of support or warranty**.  
I do **not** provide user support, bug-fixing on demand, or feature development.

If you detect a bug, please **actively participate in debugging it** (analysis,proposed fix, or pull request) **before reporting it**. Bug reports without investigation may be ignored.

🚫 **No new features will be added.**  
✅ **All types of contributions are welcome**, including bug fixes, refactoring,documentation improvements, and optimizations.

By using or contributing to this project, you acknowledge and accept these conditions.

## Supported renders (via `#tag` in header)

| Tag #silver | Effect #wrap10|
| --- | --- |
| **#badge** | Renders value as a blue pill badge |
| **#bold** | Wraps value in `<strong>` |
| **#dateshortd** | Formats dates (YYYY-MM) |
| **#dateshorty** | Formats dates (MM-DD) |
| **#datetime** | Formats full timestamp |
| **#emoji** | Converts words like “happy”, “cool”, “neutral” → 😃 😎 😐 |
| **#evaluation** | Converts 0–5 into ❤️/🤍 rating |
| **#float** | Forces 2 decimal places (e.g. “3.14”) |
| **#gauge** | Graphical percentage representation ███░ |
| **#histo** | Converts number to █ |
| **#int** | Parses and formats whole numbers with locale separators |
| **#italic** | Wraps value in `<em>` |
| **#kg** | Formats number as “12 345 kg” |
| **#km** | Formats number as “12 345 km” |
| **#link** | Turns URL into clickable link |
| **#logical** | Converts truthy → `✅` / falsy → `❌` |
| **#lower** | Forces lowercase |
| **#mood** | Converts evaluation of mood to emoji 1:bad 5: very well |
| **#percentXX** | Converts decimal to percentage (0.15 → “15 %”) or **#percent10** Converts \[0-10\] to percentage (1 → “10 %”) |
| **#posneg** | Colored gauge -2 🟥🟥, 0 ⬜, +1 🟩 |
| **#stars** | Converts number to up to 10 ⭐ stars |
| **#trend** | Converts + / - / = into 🔼 🔽 ➡️ |
| **#upper** | Forces uppercase |
| **#usd** | Formats number as “$12,345” |
| **#watt** | Formats number as “12 345 W” |
| **#euro** | Formats number as “12 345 €” |

Just add the render as a hashtag tag in your table header:

```md
| Product #wine | Euro #euro | Percent #percent | Logical #logical | Stars #stars | Evaluation #evaluation | Updated              | Mood #emoji | Trend #trend |
| ------------- | ---------- | ---------------- | ---------------- | ------------ | ---------------------- | -------------------- | ----------- | ------------ |
| Widget        | 12.99      | 0.15             | 0                | 3            | 4                      | 2025-11-06T14:30:00Z | happy       | +            |
| Gadget        | 8.50       | 0.23             | false            | 5            | 2                      | 2024-12-25T10:00:00Z | neutral     | -            |
| Thingamajig   | 5.75       | 0.05             | true             | 4            | 5                      | 2023-05-10T08:15:00Z | cool        | =            |
```

or in a query:

```lua
${query[[from index.tag "page"
    select {
      ["Page #upper"] =  ref,
      Modified = lastModified }
    order by lastModified desc limit 5]] or "No empty pages."}
```

![](https://community.silverbullet.md/uploads/default/original/2X/e/e2598b9faf8fb223eb5b68b9d03b0729384c5351.png)
![](https://community.silverbullet.md/uploads/default/original/2X/e/ec9b8a44f48b1854b94544da609e24fb1c9bf888.gif)

## How to

### Add new renderer

```lua
mls = mls or {}
mls.table = mls.table or {}
mls.table.renderer = mls.table.renderer or {}

mls.table.renderer["euro"] = {
     	completion = {
			{
				name = "one",
				value = "1"
			},
			{
				name = "two",
				value = "2"
			},
		},
		visual = [[isNaN(v) ? v : `${parseFloat(v).toLocaleString()} TEST`]],
		validation = function(v){
          --...
          return true
        }
}
```

### Private renderers (optional)

You can keep the main script public and define private renderers in a separate `space-lua` script.

#### How it works

The public script freezes its formatters into the injected JS at the moment `enableTableRenderer()` runs. So a private renderer added *after* that won't appear until the renderer is relaunched.

Give the **private** script a **lower** priority, e.g. on the first line of the block: `-- priority: 10`
Space-lua runs higher priorities first, so the public script initializes before the private one — meaning `mls.table.enableTableRenderer` exists when the private script runs.

In the **private** script: add your renderers to `mls.table.renderer[...]`, then relaunch to re-inject the formatters (the JS is frozen at enable time, so simply adding a renderer afterwards is not enough):

```lua
   mls.table.cleanupRenderer()
   mls.table.enableTableRenderer()
```

The public script stays standalone: without a private script, its autostart activates it normally.

#### Example private script

```lua
-- priority: 10
mls = mls or {}
mls.table = mls.table or {}
mls.table.renderer = mls.table.renderer or {}

mls.table.renderer["mytag"] = {
    completion = {
        { name = "Option A", value = "a" },
        { name = "Option B", value = "b" },
    },
    multiple = true,
    visual = [[return v;]],
    validation = function(v)
        return true
    end
}
```
## Code

```space-lua
-- priority: 15
-- Table Renderer (Formatter)
mls = mls or {}
mls.table = mls.table or {}
mls.table.renderer = mls.table.renderer or {}

local cfg = config.get("tableRenderer") or {}
local enabled = cfg.enabled ~= false

--------------------------------------------------
-- FUNCTION
--------------------------------------------------
function mls.table.cleanupRenderer()
	local scriptId = "sb-table-renderer-runtime"
	local existing = js.window.document.getElementById(scriptId)
	if existing then
		local ev = js.window.document.createEvent("Event")
		ev.initEvent("sb-table-renderer-unload", true, true)
		js.window.dispatchEvent(ev)
		existing.remove()
		print("Table Renderer: Disabled")
	else
		print("Table Renderer: Already inactive")
	end
end

------------------------------------------------
-- Generic validators
------------------------------------------------

local function isNumber(v)
	return tonumber(v) ~= nil
end

local function isBetween(v, min, max)
	v = tonumber(v)
	return v ~= nil and v >= min and v <= max
end

local function isInt(v)
	v = tonumber(v)
	return v ~= nil and math.floor(v) == v
end

local function isDate(v)
    if type(v) ~= "string" then return false end
    return v:match("^%d%d%d%d%-%d%d%-%d%d$") ~= nil
end

------------------------------------------------
-- Renderers (JS visual + Lua validation)
------------------------------------------------
mls.table.renderer = {
	euro = {
		visual = [[isNaN(v) ? v : `${parseFloat(v).toLocaleString()} €`]],
		validation = isNumber
	},
	usd = {
		visual = [[isNaN(v) ? v : `$${parseFloat(v).toLocaleString()}`]],
		validation = isNumber
	},
	kg = {
		visual = [[isNaN(v) ? v : `${parseFloat(v).toLocaleString()} kg`]],
		validation = isNumber
	},
	km = {
		visual = [[isNaN(v) ? v : `${parseFloat(v).toLocaleString()} km`]],
		validation = isNumber
	},
	watt = {
		visual = [[isNaN(v) ? v : `${parseFloat(v).toLocaleString()} W`]],
		validation = isNumber
	},
    percent = {
        visual = [[
            if (isNaN(v)) return v;
    
            // extract number from tag name, e.g. "percent10" -> 10
            const m = tag.match(/^percent(\d+)?$/);
            const factor = m && m[1] ? parseFloat(m[1]) : 100;
    
            return `${(parseFloat(v) * factor).toFixed(0)} %`;
        ]],
        validation = isNumber
    },
	int = {
		visual = [[isNaN(v) ? v : parseInt(v, 10).toLocaleString()]],
		validation = isInt
	},
	float = {
		visual = [[isNaN(v) ? v : parseFloat(v).toFixed(2)]],
		validation = isNumber
	},
    dateshorty = {
        visual = [[
            if (!v) return v;
            const m = v.match(/^(\d{4})-(\d{2})-(\d{2})$/);
            return m ? `${m[2]}-${m[3]}` : v;
        ]],
        validation =isDate
    },
    dateshortd = {
        visual = [[
            if (!v) return v;
            const m = v.match(/^(\d{4})-(\d{2})-(\d{2})$/);
            return m ? `${m[1]}-${m[2]}` : v;
        ]],
        validation = isDate
    },
  	upper = {
		visual = [[v.toString().toUpperCase()]],
		validation = function(v)
			return v ~= nil
		end
	},
	lower = {
		visual = [[v.toString().toLowerCase()]],
		validation = function(v)
			return v ~= nil
		end
	},
	bold = {
		visual = [[`<strong>${v}</strong>`]],
		validation = function(v)
			return v ~= nil
		end
	},
	italic = {
		visual = [[`<em>${v}</em>`]],
		validation = function(v)
			return v ~= nil
		end
	},
	link = {
		visual = [[`<a href="${v}" target="_blank">${v.replace(/^https?:\/\//, '')}</a>`]],
		validation = function(v)
			return type(v) == "string" and v:match("^https?://")
		end
	},
	logical = {
		completion = {
          	{
				name = "✅",
				value = "1"
			},
			{
				name = "❌",
				value = "0"
			},
		},
		visual = [[
      if (v !== '✅' && v !== '❌') {
        const val = v.toString().toLowerCase().trim();
        return (val === '1' || val === 'true' || val === 'yes' || val === 'ok') ? '✅' : '❌';
      }
      return v;
    ]],
		validation = function(v)
			return v ~= nil
		end
	},
	evaluation = {
		completion = {
			{
				name = "🤍🤍🤍🤍🤍",
				value = "0"
			},
			{
				name = "❤️🤍🤍🤍🤍",
				value = "1"
			},
			{
				name = "❤️❤️🤍🤍🤍",
				value = "2"
			},
			{
				name = "❤️❤️❤️🤍🤍",
				value = "3"
			},
			{
				name = "❤️❤️❤️❤️🤍",
				value = "4"
			},
			{
				name = "❤️❤️❤️❤️❤️",
				value = "5"
			},
		},
		visual = [[
      const n = parseInt(v, 10);
      if (isNaN(n)) return v;
      return '❤️'.repeat(Math.max(0, Math.min(n, 5)))
           + '🤍'.repeat(5 - Math.max(0, Math.min(n, 5)));
    ]],
		validation = function(v)
			return isBetween(v, 0, 5)
		end
	},
	histo = {
		visual = [[
      const n = parseInt(v, 10);
      return isNaN(n) ? v : '█'.repeat(n);
    ]],
		validation = isInt
	},
	gauge = {
		visual = [[
      const a = 0;
      const b = 100;
      const val = parseInt(v, 10);
      if (isNaN(val)) return v;
      const clampedValue = Math.max(a, Math.min(val, b));
      const percentage = (clampedValue - a) / (b - a);
      const filled = Math.floor(percentage * 10);
      const empty = 10 - filled;
      return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
    ]],
		validation = function(v)
			return isBetween(v, 0, 100)
		end
	},
	trend = {
		completion = {
			{
				name = "🔼",
				value = "+"
			},
			{
				name = "🔽",
				value = "-"
			},
			{
				name = "➡️",
				value = "="
			}
		},
		visual = [[
      const val = v.trim();
      if (val === '+') return '🔼';
      if (val === '-') return '🔽';
      if (val === '=') return '➡️';
      return val;
    ]],
		validation = function(v)
			return v == "+" or v == "-" or v == "="
		end
	},
	emoji = {
		completion = {
  -- basic emotions
			{
				name = "happy 😃",
				value = "happy"
			},
			{
				name = "sad 😢",
				value = "sad"
			},
			{
				name = "angry 😠",
				value = "angry"
			},
			{
				name = "love ❤️",
				value = "love"
			},
			{
				name = "neutral 😐",
				value = "neutral"
			},
			{
				name = "cool 😎",
				value = "cool"
			},

  -- positive / joyful
			{
				name = "smile 😊",
				value = "smile"
			},
			{
				name = "grin 😁",
				value = "grin"
			},
			{
				name = "laugh 😂",
				value = "laugh"
			},
			{
				name = "excited 🤩",
				value = "excited"
			},
			{
				name = "proud 😌",
				value = "proud"
			},
			{
				name = "relieved 😮‍💨",
				value = "relieved"
			},
			{
				name = "thankful 🙏",
				value = "thankful"
			},
			{
				name = "party 🥳",
				value = "party"
			},
			{
				name = "confident 😏",
				value = "confident"
			},

  -- negative / difficult
			{
				name = "cry 😭",
				value = "cry"
			},
			{
				name = "disappointed 😞",
				value = "disappointed"
			},
			{
				name = "worried 😟",
				value = "worried"
			},
			{
				name = "anxious 😰",
				value = "anxious"
			},
			{
				name = "scared 😱",
				value = "scared"
			},
			{
				name = "tired 😴",
				value = "tired"
			},
			{
				name = "sick 🤒",
				value = "sick"
			},
			{
				name = "bored 😒",
				value = "bored"
			},
			{
				name = "frustrated 😤",
				value = "frustrated"
			},
			{
				name = "confused 😕",
				value = "confused"
			},

  -- reactions
			{
				name = "surprised 😮",
				value = "surprised"
			},
			{
				name = "shocked 😲",
				value = "shocked"
			},
			{
				name = "thinking 🤔",
				value = "thinking"
			},
			{
				name = "facepalm 🤦",
				value = "facepalm"
			},
			{
				name = "shrug 🤷",
				value = "shrug"
			},
			{
				name = "eyeRoll 🙄",
				value = "eyeroll"
			},

  -- social / playful
			{
				name = "wink 😉",
				value = "wink"
			},
			{
				name = "kiss 😘",
				value = "kiss"
			},
			{
				name = "hug 🤗",
				value = "hug"
			},
			{
				name = "teasing 😜",
				value = "teasing"
			},
			{
				name = "silly 🤪",
				value = "silly"
			},

  -- approval / disapproval
			{
				name = "ok 👌",
				value = "ok"
			},
			{
				name = "thumbsUp 👍",
				value = "thumbsup"
			},
			{
				name = "thumbsDown 👎",
				value = "thumbsdown"
			},
			{
				name = "clap 👏",
				value = "clap"
			},
			{
				name = "respect 🫡",
				value = "respect"
			},

  -- status / misc
			{
				name = "fire 🔥",
				value = "fire"
			},
			{
				name = "star ⭐",
				value = "star"
			},
			{
				name = "check ✅",
				value = "check"
			},
			{
				name = "cross ❌",
				value = "cross"
			},
			{
				name = "warning ⚠️",
				value = "warning"
			},
		},
		visual = [[
      const map = {
        // basic emotions
        happy: '😃',
        sad: '😢',
        angry: '😠',
        love: '❤️',
        neutral: '😐',
        cool: '😎',

        // positive / joyful
        smile: '😊',
        grin: '😁',
        laugh: '😂',
        excited: '🤩',
        proud: '😌',
        relieved: '😮‍💨',
        thankful: '🙏',
        party: '🥳',
        confident: '😏',


        // negative / difficult
        cry: '😭',
        disappointed: '😞',
        worried: '😟',
        anxious: '😰',
        scared: '😱',
        tired: '😴',
        sick: '🤒',
        bored: '😒',
        frustrated: '😤',
        confused: '😕',

        // reactions
        surprised: '😮',
        shocked: '😲',
        thinking: '🤔',
        facepalm: '🤦',
        shrug: '🤷',
        eyeRoll: '🙄',

        // social / playful
        wink: '😉',
        kiss: '😘',
        hug: '🤗',
        teasing: '😜',
        silly: '🤪',

        // approval / disapproval
        ok: '👌',
        thumbsUp: '👍',
        thumbsDown: '👎',
        clap: '👏',
        respect: '🫡',

        // status / misc
        fire: '🔥',
        star: '⭐',
        check: '✅',
        cross: '❌',
        warning: '⚠️',
      };
      const key = v.toString().toLowerCase();
      return map[key] || v;
    ]],
		validation = function(v)
			return type(v) == "string"
		end
	},
	posneg = {
		completion = {
			{
				name = "-2 🟥🟥",
				value = "-2"
			},
			{
				name = "-1 🟥",
				value = "-1"
			},
			{
				name = "0 ⬜",
				value = "0"
			},
			{
				name = "1 🟩",
				value = "1"
			},
			{
				name = "2 🟩🟩",
				value = "2"
			},
		},
		visual = [[
      if (isNaN(v)) return v;
      const val = parseInt(v, 10);
      if (val < 0) return "🟥".repeat(Math.abs(val));
      if (val > 0) return "🟩".repeat(Math.abs(val));
      return "⬜";
    ]],
		validation = function(v)
			return isBetween(v, -10, 10)
		end
	},
	speed = {
		visual = [[v + " km/h"]],
		validation = function(v)
			return isBetween(v, 0, 300)
		end
	},
	mood = {
		completion = {
			{
				name = "😞",
				value = "1"
			},
			{
				name = "🙁",
				value = "2"
			},
			{
				name = "😐",
				value = "3"
			},
			{
				name = "🙂",
				value = "4"
			},
			{
				name = "😄",
				value = "5"
			},
		},
		visual = [[
    const n = parseInt(v, 10);
    const moodScaleSoft = ['😔', '🙁', '😐', '🙂', '😄'];
    return moodScaleSoft[(n - 1) % 5];
  ]],
		validation = function(v)
			return isBetween(v, 1, 5)
		end
	},
	stars = {
		completion = {
			{
				name = "1 ⭐",
				value = "1"
			},
			{
				name = "2 ⭐⭐",
				value = "2"
			},
			{
				name = "3 ⭐⭐⭐",
				value = "3"
			},
			{
				name = "4 ⭐⭐⭐⭐",
				value = "4"
			},
			{
				name = "5 ⭐⭐⭐⭐⭐",
				value = "5"
			},
			{
				name = "6 ⭐⭐⭐⭐⭐⭐",
				value = "6"
			},
			{
				name = "7 ⭐⭐⭐⭐⭐⭐⭐",
				value = "7"
			},
			{
				name = "8 ⭐⭐⭐⭐⭐⭐⭐⭐",
				value = "8"
			},
			{
				name = "9 ⭐⭐⭐⭐⭐⭐⭐⭐⭐",
				value = "9"
			},
			{
				name = "10 ⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐",
				value = "10"
			},
		},
		visual = [[
    const n = parseInt(v, 10);
    return isNaN(n) ? v : '⭐'.repeat(Math.max(0, Math.min(n, 10)));
  ]],
		validation = function(v)
			return isBetween(v, 1, 10)
		end
	},
    badge = {
        completion = {},
        visual = [[
    return `<span style="background:#2196f3;color:white;padding:2px 6px;border-radius:8px;font-size:0.9em;">${v}</span>`;
    ]],
        validation = function(v)
            return v ~= nil
        end
    },
}

------------------------------------------------
-- Dispatcher
------------------------------------------------
mls.table.render = function(label, rendererN)
    local value=""
	local rendererName = rendererN
	if (type(rendererN) == "table") then
		for _, tag in ipairs(rendererN) do
			if mls.table.renderer[tag] then
				rendererName = tag
			end
		end
	end

    if not label and not rendererName then
      editor.flashNotification("Missing renderer: " .. tostring(rendererName), "error")
	elseif label then
        local input=""
        rendererName= rendererName or ""
        local renderer = mls.table.renderer[rendererName]
        if renderer then
    		if renderer.completion and # renderer.completion > 0 and not renderer.multiple then
            	input = editor.filterBox(label, renderer.completion)
            elseif renderer.completion and # renderer.completion > 0 and renderer.multiple then 
                input = mls.ui.filterBoxMulti(label, renderer.completion)
    		else
                input = editor.prompt(label)
            end
        else
          input = editor.prompt(label)
		end
		value = input
		if input and input.value then
			value = input.value
		end
		if renderer and renderer.validation and not renderer.validation(value) then
			editor.flashNotification("Input not valid: " .. tostring(label).."/"..value, "error")
		end
	end
    return value
end

------------------------------------------------
-- JS generator
------------------------------------------------
local function exportJSFormatters(renderers)
	local lines = {}
	table.insert(lines, "const formatters = {")
	for name, def in pairs(renderers) do
		if def.visual then
			local js = def.visual
      -- single line or block
			if string.match(js, "\n") then
				table.insert(lines, "  " .. name .. ": (v,tag) => {")
				table.insert(lines, js)
				table.insert(lines, "  },")
			else
				table.insert(lines, "  " .. name .. ": v => " .. js .. ",")
			end
		end
	end
	table.insert(lines, "};")
	return table.concat(lines, "\n")
end

--------------------------------------------------
-- ENABLE
--------------------------------------------------

function mls.table.enableTableRenderer()
	local scriptId = "sb-table-renderer-runtime"
	if js.window.document.getElementById(scriptId) then
		print("Table Renderer: Already active")
		return
	end
	local scriptEl = js.window.document.createElement("script")
	scriptEl.id = scriptId
	scriptEl.innerHTML = [[
(function () {
  'use strict';

  const DEBUG = false;
  const log = (...a) => DEBUG && console.log('[sb-table-renderer]', ...a);

  /* ---------------- FORMATTERS ---------------- */
  // formatters injected here
]] .. exportJSFormatters(mls.table.renderer) .. [[

  /* ---------------- CORE ---------------- */

  // Extract formats once per table
  function extractFormats(table) {
    const formats = [];
    const headerRow = table.querySelector('thead tr') || table.querySelector('tr');
    if (!headerRow) return formats;

    const headerCells = headerRow.cells;
    for (let idx = 0; idx < headerCells.length; idx++) {
      formats[idx] = null;
      const cell = headerCells[idx];
      const tags = cell.querySelectorAll('a.hashtag,[data-tag-name]');
      for (let t = 0; t < tags.length; t++) {
        const tagEl = tags[t];
        const name = tagEl.dataset?.tagName || tagEl.textContent.replace('#', '');
        const base = name.replace(/\d+$/, '');
        if (formatters[base]) {
          formats[idx] = name;   // keep full tag: wrap20
          tagEl.style.display = 'none';
          break;
        }
      }
    }

    return formats;
  }

  // Process table cells
  function processTable(table) {
    if (table.dataset.sbFormatted) return;

    const formats = extractFormats(table);
    const tbody = table.tBodies[0];
    if (!tbody) return;

    const rows = tbody.rows;
    for (let r = 0; r < rows.length; r++) {
      const cells = rows[r].cells;
      for (let c = 0; c < cells.length; c++) {
        const fmt = formats[c];
        if (!fmt) continue;

        const cell = cells[c];
        const raw = cell.textContent.trim();

        // cache base -> formatter
        const base = fmt.replace(/\d+$/, '');
        const formatter = formatters[base];
        if (!formatter) continue;

        const out = formatter(raw, fmt);
        if (out !== raw) {
          cell.textContent = out;
          cell.dataset.sbformatted = fmt;
        }
      }
    }

    table.dataset.sbFormatted = 'true';
  }

  // Scan for tables that need formatting
  function scan() {
    const tables = document.querySelectorAll('#sb-editor table:not([data-sb-formatted])');
    for (let i = 0; i < tables.length; i++) {
      processTable(tables[i]);
    }
  }

  /* ---------------- OBSERVER ---------------- */

  // debounce MutationObserver to avoid repeated calls
  let scanTimeout;
  const observer = new MutationObserver(() => {
    if (scanTimeout) clearTimeout(scanTimeout);
    scanTimeout = setTimeout(scan, 50);
  });
  observer.observe(document.body, { childList: true, subtree: true });

  scan();

  /* ---------------- CLEANUP ---------------- */

  window.addEventListener('sb-table-renderer-unload', function cln() {
    observer.disconnect();

    const formattedCells = document.querySelectorAll('[data-sbformatted]');
    for (let i = 0; i < formattedCells.length; i++) {
      formattedCells[i].removeAttribute('data-sbformatted');
    }

    const formattedTables = document.querySelectorAll('table[data-sb-formatted]');
    for (let i = 0; i < formattedTables.length; i++) {
      delete formattedTables[i].dataset.sbFormatted;
    }

    window.removeEventListener('sb-table-renderer-unload', cln);
  });

})();
  ]]
	js.window.document.body.appendChild(scriptEl)
end

--------------------------------------------------
-- COMMANDS
--------------------------------------------------

command.define{
	name = "Table: Enable Renderer",
	run = function()
		mls.table.enableTableRenderer()
	end
}

command.define{
	name = "Table: Disable Renderer",
	run = function()
		mls.table.cleanupRenderer()
	end
}
--------------------------------------------------
-- AUTOSTART
--------------------------------------------------
if enabled then
	mls.table.enableTableRenderer()
else
	mls.table.cleanupRenderer()
end
```

## Changelog

* 2026-02-13:
  * feat:remove date and add `dateshorty` and `dateshortd` for format date  
  * feat: supoprt tag([0-9]+) format
- 2026-02-06:
    - fix: evaluation renderer [0-5] instead [1-5]
    - fix: render function
    - fix: “logical” return [0,1]
- 2026-02-01
    - feat: define renderers in lua
    - feat: add validation mechanism
    - feat: add completion mechanism
- 2026-01-24:
    - feat: convert to space-lua
    - feat: add renderers (mood, emoj)
- 2026-01-02 feat: add kg, km, watt, histo

## Community

[Silverbullet forum](https://community.silverbullet.md/t/md-table-renderers/3545/15)
