srijeda, 11. novembar 2026. · Godina I · Broj 15 · Cijena: 0 dinara
JUGOSLOVENSKI FORUM

Glasilo YU mreže · NEMA GRANICA ZA ZNANJE

Prijava Upis
Pravo na uvid u kod. Otvoren protokol omogućava nezavisne operatere i interoperabilnost. Manifest

Izvor

4d7883a894b3

   1 // Safe markdown-lite: everything is HTML-escaped first; only a small fixed set of constructs becomes markup.
   2 export const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
   3 
   4 function inline(text, mentions) {
   5   const codes = [];
   6   let s = esc(text).replace(/`([^`\n]{1,300})`/g, (_, c) => { codes.push(c); return `\u0000${codes.length - 1}\u0000`; });
   7   s = s.replace(/\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]{1,500})\)/g, (_, label, url) =>
   8     `<a href="${url}" rel="nofollow ugc noopener noreferrer">${label}</a>`);
   9   s = s.replace(/(^|[\s(])(https?:\/\/[^\s<]{3,500}[^\s<.,;:!?)'"])/g, (_, pre, url) =>
  10     `${pre}<a href="${url}" rel="nofollow ugc noopener noreferrer">${url}</a>`);
  11   s = s.replace(/\*\*([^*\n]{1,500})\*\*/g, '<strong>$1</strong>');
  12   s = s.replace(/(^|[^*\w])\*([^*\n]{1,500})\*(?!\w)/g, '$1<em>$2</em>');
  13   s = s.replace(/(^|[^\w@/])@([a-z0-9_]{3,20})\b/gi, (m, pre, h) => {
  14     if (mentions) mentions.add(h.toLowerCase());
  15     return `${pre}<a class="mention" href="/u/${h.toLowerCase()}">@${h}</a>`;
  16   });
  17   return s.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code>${codes[+i]}</code>`);
  18 }
  19 
  20 export function render(src, mentions) {
  21   const lines = String(src ?? '').replace(/\r\n?/g, '\n').split('\n');
  22   const out = [];
  23   let para = [], list = [], quote = [];
  24   const flushPara = () => { if (para.length) out.push(`<p>${para.map((l) => inline(l, mentions)).join('<br>')}</p>`); para = []; };
  25   const flushList = () => { if (list.length) out.push(`<ul>${list.map((l) => `<li>${inline(l, mentions)}</li>`).join('')}</ul>`); list = []; };
  26   const flushQuote = () => { if (quote.length) out.push(`<blockquote>${render(quote.join('\n'), mentions)}</blockquote>`); quote = []; };
  27   const flush = () => { flushPara(); flushList(); flushQuote(); };
  28   for (let i = 0; i < lines.length; i++) {
  29     const line = lines[i];
  30     if (/^```/.test(line)) {
  31       flush();
  32       const code = [];
  33       for (i++; i < lines.length && !/^```/.test(lines[i]); i++) code.push(lines[i]);
  34       out.push(`<pre><code>${esc(code.join('\n'))}</code></pre>`);
  35       continue;
  36     }
  37     if (/^>\s?/.test(line)) { flushPara(); flushList(); quote.push(line.replace(/^>\s?/, '')); continue; }
  38     if (/^\s*[-*]\s+/.test(line)) { flushPara(); flushQuote(); list.push(line.replace(/^\s*[-*]\s+/, '')); continue; }
  39     if (!line.trim()) { flush(); continue; }
  40     flushList(); flushQuote();
  41     para.push(line);
  42   }
  43   flush();
  44   return out.join('\n');
  45 }
  46 
  47 export function excerpt(src, n = 160) {
  48   const t = String(src ?? '').replace(/```[\s\S]*?```/g, ' ').replace(/[*`>#\[\]]/g, '').replace(/\((https?:[^)]+)\)/g, '').replace(/\s+/g, ' ').trim();
  49   return t.length > n ? t.slice(0, n - 1).replace(/\s+\S*$/, '') + '...' : t;
  50 }
  51