Md Table renderers

md table renderers for SB

To my mind, md table renderers could be improved to have something really nice.
Inspired by @Mr.Red plugin and Microsoft Loop (evil),I have tried to do it in lua but I have failed to implement it.
I use often userscript to improve website ux and i have created a script to improve table render.
Rendering converts from :

| 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   | =     |

to

Fun and beautiful!!!

Code


// ==UserScript==
// @name        Table renderer
// @namespace   Violentmonkey Scripts
// @match       *
// @grant       none
// @version     1.0
// @author      -
// @description 06/11/2025 16:32:22
// ==/UserScript==


(function () {
  'use strict';

  // ---------- Configuration ----------
  const DEBUG = false;             // set true to see console logs
  const POLL_FALLBACK_MS = 1500;   // fallback polling interval if observer misfires
  const DEBOUNCE_MS = 500;         // debounce for batch mutation handling

  // ---------- Formatters ----------
 const formatters = {
    euro: v => isNaN(v) ? v : `${parseFloat(v).toLocaleString()} €`,
    usd: v => isNaN(v) ? v : `$${parseFloat(v).toLocaleString()}`,
    percent: v => isNaN(v) ? v : `${(parseFloat(v) * 100).toFixed(0)} %`,
    int: v => isNaN(v) ? v : parseInt(v, 10).toLocaleString(),
    float: v => isNaN(v) ? v : parseFloat(v).toFixed(2),
    upper: v => v.toString().toUpperCase(),
    lower: v => v.toString().toLowerCase(),
    bold: v => `<strong>${v}</strong>`,
    italic: v => `<em>${v}</em>`,
    link: v => `<a href="${v}" target="_blank">${v.replace(/^https?:\/\//, '')}</a>`,
    date: v => formatDate(v),
    datetime: v => formatDateTime(v),
    logical: v => {
      if(v !=='✅' &&  v !=='❌'){
        const val = v.toString().toLowerCase().trim();
        return (val === '1' || val === 'true' || val === 'yes' || val === 'ok') ? '✅' : '❌';
      }
      return v;
    },
    stars: v => {
      const n = parseInt(v, 10);
      return isNaN(n) ? v : '⭐'.repeat(Math.max(0, Math.min(n, 10)));
    },
    evaluation: v => {
      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)));
    },
    badge: v => `<span style="background:#2196f3;color:white;padding:2px 6px;border-radius:8px;font-size:0.9em;">${v}</span>`,
    emoji: v => {
      const map = { happy: '😃', sad: '😢', cool: '😎', angry: '😠', love: '❤️', neutral: '😐' };
      const key = v.toString().toLowerCase();
      return map[key] || v;
    },
    trend: v => {
      const val = v.trim();
      if (val === '+') return '🔼';
      if (val === '-') return '🔽';
      if (val === '=') return '➡️';
      return val;
    },
  };

  // ---------- Utilities ----------
  function log(...args) { if (DEBUG) console.log('[sb-table]', ...args); }
  function escapeHtml(str) {
    return String(str)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }
  function escapeAttr(str) {
    return String(str).replace(/"/g, '%22');
  }

  // Create a DOM element for formatted output safely
  function createFormattedNode(value, fmt) {
    const fn = formatters[fmt];
    if (!fn) return document.createTextNode(value);
    const html = fn(value);
    // If formatter returned plain text (no tags) we can put textContent,
    // otherwise use innerHTML (we escaped where needed above)
    const container = document.createElement('span');
    // Heuristic: if result contains angle brackets assume it's HTML-safe from our escapes
    if (/<[a-z][\s\S]*>/i.test(html)) container.innerHTML = html;
    else container.textContent = html;
    return container;
  }

  // Debounce helper
  function debounce(fn, ms) {
    let t = null;
    return function (...args) {
      clearTimeout(t);
      t = setTimeout(() => fn.apply(this, args), ms);
    };
  }

  // ---------- Core processing ----------
  // applying flag prevents reacting to our own DOM writes
  let applying = false;

  function processTables(container) {
    if (!container) return;
    if (applying) return;
    applying = true;
    try {
      log('processTables start');
      const tables = container.querySelectorAll('table');
      tables.forEach(table => {
        // find header cells (thead preferred, fallback to first row)
        let headerCells = table.querySelectorAll('thead tr:first-child th, thead tr:first-child td');
        if (!headerCells || headerCells.length === 0) {
          // fallback: first row of tbody or first tr in table
          const firstRow = table.querySelector('tr');
          headerCells = firstRow ? firstRow.querySelectorAll('th, td') : [];
        }
        if (!headerCells || headerCells.length === 0) return;

        // build col formatter list
        const colFormats = Array.from(headerCells).map((cell) => {
          // look for hashtag link by class or data attribute
          const tagLinks = cell.querySelectorAll('a.hashtag, a.sb-hashtag, [data-tag-name]');
          for(let i=0;i<tagLinks.length;i++){
            let tagLink=tagLinks[i]
            if (Object.keys(formatters).includes(tagLink.dataset.tagName)) {
              const name = (tagLink.dataset && tagLink.dataset.tagName) ? tagLink.dataset.tagName
                        : (tagLink.getAttribute ? tagLink.getAttribute('data-tag-name') : null);
              if (name) {
                // hide the hashtag visually but keep it in DOM (so editor can still find it)
                tagLink.style.display = 'none';
                return String(name).trim().toLowerCase();
              }
            }
         }
        return null;
        });

        // apply to body rows (tbody preferred)
        const bodyRows = table.querySelectorAll('tbody tr');
        const rows = bodyRows.length ? bodyRows : table.querySelectorAll('tr');
        rows.forEach(row => {
          const cells = row.querySelectorAll('td, th'); // format cells regardless of tag
          cells.forEach((cell, idx) => {
            const fmt = colFormats[idx];
            if (!fmt || !formatters[fmt]) return;
            const raw = cell.textContent.trim();

            // If the cell already contains a formatted node produced by us, we may replace it
            // But avoid replacing while user is actively typing inside the same cell (contentEditable)
            // If the cell or an ancestor is currently focused, skip (user editing)
            const active = document.activeElement;
            if (active && (cell === active || cell.contains(active))) {
              log('skip formatting because user is editing', cell, raw);
              return;
            }

            // Generate formatted node and replace contents
            const formattedNode = createFormattedNode(raw, fmt);
            // Quick check: if cell already equals formattedNode.textContent (idempotent), skip
            const candidateText = (formattedNode.textContent || '').trim();
            if (candidateText === raw && !/<[a-z][\s\S]*>/i.test(formattedNode.innerHTML)) {
              // Nothing to change (formatter didn't alter text)
              return;
            }

            // Replace content safely
            cell.innerHTML = '';            // wipe
            cell.appendChild(formattedNode);
            // mark as processed (for debugging/inspection)
            cell.setAttribute('data-sbformatted', fmt);
          });
        });
      });
      log('processTables done');
    } catch (err) {
      console.error('sb-table: processing error', err);
    } finally {
      // tiny timeout to ensure observer won't see our writes as immediate external mutations
      setTimeout(() => { applying = false; }, 20);
    }
  }

  const debouncedProcess = debounce(processTables, DEBOUNCE_MS);

  // ---------- Setup: wait for #sb-editor ----------
  function waitForEditorAndStart() {
    const editor = document.getElementById('sb-editor');
    if (!editor) {
      log('#sb-editor not found, retrying...');
      setTimeout(waitForEditorAndStart, 300);
      return;
    }
    startWatching(editor);
  }

  // ---------- Start watching container ----------
  let observer = null;
  let fallbackInterval = null;
  let lastRun = 0;

  function startWatching(editor) {
    // initial run once editor appears (give editor a moment)
    setTimeout(() => debouncedProcess(editor), 400);

    // listen to input events (contenteditable emits input)
    editor.addEventListener('input', () => {
      log('input event -> schedule process');
      debouncedProcess(editor);
    }, { passive: true });

    // also listen to keyup/paste to catch edge cases
    editor.addEventListener('keyup', () => debouncedProcess(editor));
    editor.addEventListener('paste', () => setTimeout(() => debouncedProcess(editor), 80));

    // MutationObserver config - broad to catch replacements/attribute changes
    const config = { childList: true, subtree: true, characterData: true, attributes: true };

    // Create observer
    observer = new MutationObserver((mutations) => {
      if (applying) return;
      // Quick heuristic: if many mutations, debounce
      const now = Date.now();
      // Avoid calling too often in rapid mutation bursts
      if (now - lastRun < (DEBOUNCE_MS / 2)) {
        debouncedProcess(editor);
      } else {
        debouncedProcess(editor);
        lastRun = now;
      }
    });

    try {
      observer.observe(editor, config);
      log('MutationObserver attached to #sb-editor');
    } catch (err) {
      console.warn('sb-table: MutationObserver attach failed, falling back to polling', err);
    }

    // Fallback polling in case the environment continually replaces the editor root
    fallbackInterval = setInterval(() => {
      try {
        // ensure observer still connected, else try to reattach
        if (observer && observer.takeRecords) {
          // run periodic processing in case something missed
          debouncedProcess(editor);
        } else {
          debouncedProcess(editor);
        }
      } catch (e) {
        console.warn('sb-table: poll fallback err', e);
      }
    }, POLL_FALLBACK_MS);

    // As a safety, also observe document.body so we can detect the editor being replaced
    const bodyObserver = new MutationObserver(() => {
      const currentEditor = document.getElementById('sb-editor');
      if (currentEditor && currentEditor !== editor) {
        log('editor root replaced; reattaching to new #sb-editor');
        // cleanup old observer and restart on new editor
        try { if (observer) observer.disconnect(); } catch {}
        try { if (fallbackInterval) clearInterval(fallbackInterval); } catch {}
        startWatching(currentEditor);
      }
    });
    bodyObserver.observe(document.body, { childList: true, subtree: true });

    // Try one extra processing after a little while to catch delayed renders
    setTimeout(() => debouncedProcess(editor), 1200);
  }

  // Kickoff
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', waitForEditorAndStart);
  } else {
    waitForEditorAndStart();
  }

})();

Feel free to improve it and to share it.
The ultimate goal should to implement it in SB but it’s over my skill.

2 Likes

:hammer_and_wrench: Installation
Navigate to your Library Manager inside Silverbullet
Add my repository: silverbullet-libraries/Repository/Malys.md at main · malys/silverbullet-libraries · GitHub
Add any script my repository

1 Like

HH, you made the same mistake… (though partially credited to the present parsing system)

Lua error: Could not fetch: github:malys/silverbullet-libraries/GitHistory.md
Lua error: Could not fetch: github:malys/silverbullet-libraries/MdTableRender.md

I once had the same problem, the solution I employed:

uri: github:malys/silverbullet-libraries/GitHistory.md

uri: https://github.com/malys/silverbullet-libraries/blob/main/src/GitHistory.md

This issue appears to originate from the requirement that, for github: resolution to succeed, the .md library must reside in the GitHub repository’s root directory like @Mr.Red’s case.

Tomorrow, i Will fix it. thanks.

While you fix it:
I just found out, that your Repo was not shown under the Repos Header in the Library Manager.
After adding:

name: "Repository/Malys"
tags: meta/repository

To the frontmatter in the Malys.md file, it worked. Maybe you could add that too.

And after applying the above workaround by changing the URI/URL now the library manager complaints “Library frontmatter validation error: must have required property ‘name’”.
So this might also be worth to fix :wink:

Thanks a lot!

(All this reminds me of leveraging my own repo to work with the Library Manager, so thanks again for already doing the effort with yours :slight_smile: )

2 Likes

I don’t use edge version. Could you confirm that is working fine? :flamingo::flamingo::flamingo: @zeus-web @ChenZhu-Xie

silverbullet-libraries/scripts at main · malys/silverbullet-libraries · GitHub contains scripts to generate readme.md and malys.md. With small changes, it will work for everybody.

1 Like

How can someone live without the edge version? :slight_smile: All the interesting (new) features to explore every time. At least I look forward to every single update :wink: (and of course sometimes something “breaks” and must be fixed… Life would be boring, without :-))
Well, I did try to stay on the releases for a while, too, but then I really missed the new stuff…

OK, i see the links are working basically now. And I see several Flamingos now :wink: Thanks!

However, while trying to install e.g. Githistory, SB still complains: "Library frontmatter validation error: must have required property ‘name’ "
I’m afraid, every readme.md must contain a name tag in frontmatter, too. At least as far as I understood :slight_smile:

1 Like

I use SB as main note app (professional and personal). I need stability to use it every day.

Changelog:

:hammer_and_wrench: Installation

  1. Navigate to your Library Manager inside Silverbullet
  2. Add my repository: https://github.com/malys/silverbullet-libraries/blob/main/Repository/malys.md
  3. Add any script my repository

@zeus-web please, could you validate?

Hope, you noticed my “invisible” irony tags in my post :slight_smile:
Of course I understand the need for stable versions in reality, too :slight_smile:
Indeed I use SB also on daily basis as well private and business.
But it seems, my curiosity overweights my carefulness most of the time :wink:

Back to the topic:
Yes, now it works like a charm! Thanks a lot for fixing. I’m so courious to try the github Lib (as well as some others sounding interesting).
Only one litte cosmetic issue:
I get an error “‘marp.source’ configuration not set” with the github Lib. But seems, there is only the errormessage wrong. Obviously it looks for “history.source” :slight_smile:

Maybe you could add a little example to your description what we should configure here?

Now I’m gonna try the Github History itself. Thanks again!

After every update, I have to fix some behavior changes.
I’m very curious but sometimes, I think that I spend too many hours on coding my note app instead of writing notes.

I get an error “‘marp.source’ configuration not set” with the github Lib. But seems, there is only the errormessage wrong. Obviously it looks for “history.source”

it’s a dead code from my numerous previous intents.
Update!

for GitHistory, no configuration needed.

1 Like

feedback: now your repo and libs can be added normally

and yes, the side effect of SB’s allowing dinamical progressive programming is easily programming too much… -_-|| especially when one is expected (by default) to shoulder responsibility for others -_-|| (does not come easily to a lazy INTP)

On win11, SB 2.3.0, Brave browser
the Library MdTableRender don’t work.
I installed the js script with OrangeMonkey.

It’s working in SB 2.3.0 in Chrome. Try to debug on Brave.

There is a message…


The Content Security Policy (CSP) prevents the evaluation of arbitrary strings as JavaScript to make it more difficult for an attacker to inject unathorized code on your site.

To solve this issue, avoid using eval(), new Function(), setTimeout([string], …) and setInterval([string], …) for evaluating strings.

If you absolutely must: you can enable string evaluation by adding unsafe-eval as an allowed source in a script-src directive.

:warning: Allowing string evaluation comes at the risk of inline script injection.

1 directive
Source location Directive Status
script-src blocked

I can’t help you. I don’t use Brave.
Problably change security level for your silverbullet site or use chrome like