Lo del dev, tal cual desde el nodejs-project de su APK oficial 1.3.5, mezclado a tres bandas con la 1.3.3 como base. Lo que no habiamos tocado se coge entero. Conflictos: - backend.js: su middleware nuevo de media va delante de nuestro router. - main_views.js y bookmark_view.js: lo suyo. Su menu de tarjeta (⋯) ya aparta borrar del pulgar, que era lo que hacia nuestro cambio. - settings_view.js y tribes_view.js: lo suyo mas nuestra linea de enganche. La 1.3.5 trae una cabecera de modulo para el movil (apkModbar) que se lleva la primera caja de fichas, el buscador y los contadores. Con el skin Activista su CSS (OasisMobile.css) no se carga: se le da estilo en mobile_ux.css, los avisos suben encima con order y se ajusta la cabecera nueva de las tarjetas. Filtrar va ahora dentro de la caja de fichas (casilla + etiqueta) para viajar con ella. Perfiles: Minimo (6 modulos, de serie), Acampada (16), Activista (24, con tribus, votaciones, campañas, encuestas y tareas) y despues los del dev por numero de modulos.
338 lines
19 KiB
JavaScript
338 lines
19 KiB
JavaScript
const { a, img, video, audio, span, strong, em, u, s: strike, code, div, textarea, object } = require("../server/node_modules/hyperaxe");
|
|
const i18nBase = require("../client/assets/translations/i18n");
|
|
const { WIKILINK_RE, slugify, linkTarget } = require("../models/wiki_model");
|
|
|
|
let zoomSeq = 0;
|
|
|
|
function getI18n() {
|
|
try {
|
|
const { i18n } = require("../views/main_views");
|
|
return i18n;
|
|
} catch (_) {
|
|
return i18nBase['en'] || {};
|
|
}
|
|
}
|
|
|
|
const ESCAPE_RE = /\\([\\*_~`#>\-\[\]!])/g;
|
|
const CODE_BLOCK_RE = /```([\s\S]*?)```[ \t]*(?:\n(?:[ \t]*\n)*|$)/g;
|
|
const RULE_RE = /^[ \t]*---[ \t]*(?:\n(?:[ \t]*\n)*|$)/gm;
|
|
const HEADER_CLOSED_RE = /^[ \t]*(#{1,6})[ \t]*([^\n]{1,200}?)[ \t]*\1[ \t]*(?:\n(?:[ \t]*\n)*|$)/gm;
|
|
const HEADER_SPACED_RE = /^[ \t]*(#{1,6})[ \t]+([^\n]{1,200}?)[ \t]*#*[ \t]*(?:\n(?:[ \t]*\n)*|$)/gm;
|
|
const QUOTE_RE = /^[ \t]*>[ \t]?([^\n]*)(?:\n(?:[ \t]*\n)*|$)/gm;
|
|
const BULLET_RE = /^([ \t]*)-[ \t]+([^\n]+)(?:\n(?:[ \t]*\n)*|$)/gm;
|
|
const NUMBER_RE = /^([ \t]*)(\d{1,3})([.)])[ \t]+([^\n]+)(?:\n(?:[ \t]*\n)*|$)/gm;
|
|
const BOLD_RE = /\*\*(?!\s)((?:[^*\n]|\*(?!\*))+?)(?<!\s)\*\*/g;
|
|
const UNDERLINE_RE = /(?<![A-Za-z0-9])__(?!\s)((?:[^_\n]|_(?!_))+?)(?<!\s)__(?![A-Za-z0-9])/g;
|
|
const STRIKE_RE = /~~(?!\s)([^~\n]+?)(?<!\s)~~/g;
|
|
const ITALIC_RE = /\*(?!\s)((?:[^*\n]|\*\*)+?)(?<!\s)\*(?!\*)/g;
|
|
const INLINE_CODE_RE = /`([^`\n]+)`/g;
|
|
const BLOB_IMAGE_RE = /!\[([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
|
const BLOB_VIDEO_RE = /\[video:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
|
const BLOB_AUDIO_RE = /\[audio:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
|
const BLOB_PDF_RE = /\[pdf:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
|
const BLOB_TORRENT_RE = /\[torrent:([^\]\[\n]{0,160})\]\(\s*(&[^)\s]{1,120}\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
|
const BLOB_FILE_RE = /(?<!!)\[(?!(?:video|audio|pdf|torrent):)([^\]\n]{1,160})\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]{1,600})\s*\)/g;
|
|
const MD_MENTION_RE = /\[@([^\]\[\n]{1,120})\]\(@?([A-Za-z0-9+/=.\-]{1,120}\.ed25519)\)/g;
|
|
const RAW_MENTION_RE = /@([A-Za-z0-9+/=.\-]+\.ed25519)/g;
|
|
const BARE_MENTION_RE = /(^|[^\w\u0080-\uFFFF\[\]/@])@([\w\u0080-\uFFFF.\-]{1,64})(?=$|[^\w\u0080-\uFFFF/+=])/g;
|
|
let mentionResolver = null;
|
|
const setMentionResolver = (fn) => { mentionResolver = typeof fn === 'function' ? fn : null; };
|
|
const bareMentionName = (token) => (/\.ed25519$/i.test(token) ? '' : token.replace(/[.\-]+$/, ''));
|
|
const MSG_REF_RE = /%[A-Za-z0-9+/=]{44}\.sha256/g;
|
|
const MD_LINK_RE = /!?\[([^\]\n]{1,160})\]\((https?:\/\/[^)\s]+|\/(?![\/\\])[^)\s]*)\)/g;
|
|
const URL_RE = /\b(?:https?:\/\/|www\.)[^\s<>"'`]+/gi;
|
|
const EMAIL_RE = /\b[A-Za-z0-9][A-Za-z0-9._%+-]{0,63}@(?:[A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,24}\b/g;
|
|
const HASHTAG_RE = /#[\w\u0080-\uFFFF]{1,32}(?![\w\u0080-\uFFFF])/g;
|
|
const URL_TAIL_RE = /[.,;:!?»"')\]}>]+$/;
|
|
const SELF_HOST_RE = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:[/?#]|$)/i;
|
|
const INTERNAL_PATHS = [
|
|
'agenda','ai','audios','author','backup','banking','blockexplorer','blogs','bookmarks','calendars','campaigns',
|
|
'chats','cipher','courts','cv','dev','docs','emergencies','events','favorites','feed','files','forum','games','graphos',
|
|
'hashtag','housing','images','industry','inbox','inhabitants','invites','jobs','larp','logistics','logs','mailing',
|
|
'maps','market','melody','mentions','modules','multiverse','opinions','pads','parliament','rooms','peers','pixelia','pm',
|
|
'podcasts','polls','popular','profile','projects','publish','reports','school','search','settings','shops','spread',
|
|
'stats','tags','tasks','thread','torrents','transfers','trending','tribes','tribe','videos','votes','votations','wallet','wiki'
|
|
];
|
|
const INTERNAL_PATH_SET = new Set(INTERNAL_PATHS);
|
|
const SSB_ID_RE = /^(?:%[A-Za-z0-9+/=]{20,}\.sha256|@[A-Za-z0-9+/=]{20,}\.ed25519|&[A-Za-z0-9+/=]{20,}\.sha256)$/;
|
|
|
|
const internalTargetOf = (url) => {
|
|
let parsed = null;
|
|
try { parsed = new URL(url); } catch (_) { return null; }
|
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;
|
|
const segments = parsed.pathname.split('/').filter(Boolean).map(seg => { try { return decodeURIComponent(seg); } catch (_) { return seg; } });
|
|
if (!segments.length || !INTERNAL_PATH_SET.has(segments[0].toLowerCase())) return null;
|
|
const sameHost = SELF_HOST_RE.test(url);
|
|
const hasSsbId = segments.slice(1).some(seg => SSB_ID_RE.test(seg));
|
|
if (!sameHost && !hasSsbId) return null;
|
|
const id = segments.slice(1).find(seg => SSB_ID_RE.test(seg)) || segments[1] || '';
|
|
return { href: `${parsed.pathname}${parsed.search}${parsed.hash}`, kind: segments[0].toLowerCase(), id };
|
|
};
|
|
|
|
const shortInternalId = (id) => {
|
|
const value = String(id || '');
|
|
if (!value) return '';
|
|
const sigil = /^[%@&]/.test(value) ? value[0] : '';
|
|
const body = sigil ? value.slice(1) : value;
|
|
return body.length > 8 ? `${sigil}${body.slice(0, 8)}…` : `${sigil}${body}`;
|
|
};
|
|
const MAX_DEPTH = 8;
|
|
|
|
const headerLevel = (hashes) => Math.min(3, String(hashes || '#').length);
|
|
|
|
const indentLevel = (indent) => {
|
|
const width = String(indent || '').replace(/\t/g, ' ').length;
|
|
return Math.min(4, Math.floor(width / 2) + 1);
|
|
};
|
|
|
|
const codeBlockBody = (raw) => {
|
|
const s = String(raw);
|
|
const nl = s.indexOf('\n');
|
|
if (nl === -1) return s;
|
|
return s.slice(nl + 1).replace(/\n[ \t]*$/, '');
|
|
};
|
|
|
|
function renderStyledText(value, opts = {}) {
|
|
if (typeof value !== 'string') return value === null || value === undefined ? [] : [String(value)];
|
|
const text = value.indexOf('\r') === -1 ? value : value.replace(/\r\n?/g, '\n');
|
|
const depth = Number(opts.depth) || 0;
|
|
if (depth > MAX_DEPTH) return [text];
|
|
const allMatches = [];
|
|
const push = (m, extra) => allMatches.push({ index: m.index, length: m[0].length, ...extra });
|
|
for (const m of text.matchAll(ESCAPE_RE)) push(m, { type: 'escaped', body: m[1] });
|
|
for (const m of text.matchAll(WIKILINK_RE)) push(m, { type: 'wikilink', target: linkTarget(m[1]), label: (m[2] || linkTarget(m[1])).trim() });
|
|
for (const m of text.matchAll(CODE_BLOCK_RE)) push(m, { type: 'code-block', body: codeBlockBody(m[1]) });
|
|
for (const m of text.matchAll(RULE_RE)) push(m, { type: 'rule' });
|
|
for (const m of text.matchAll(HEADER_CLOSED_RE)) push(m, { type: 'header', level: headerLevel(m[1]), body: m[2] });
|
|
for (const m of text.matchAll(HEADER_SPACED_RE)) push(m, { type: 'header', level: headerLevel(m[1]), body: m[2] });
|
|
for (const m of text.matchAll(QUOTE_RE)) push(m, { type: 'quote', body: m[1] });
|
|
for (const m of text.matchAll(BULLET_RE)) push(m, { type: 'bullet', level: indentLevel(m[1]), body: m[2] });
|
|
for (const m of text.matchAll(NUMBER_RE)) push(m, { type: 'number', level: indentLevel(m[1]), marker: `${m[2]}${m[3]}`, body: m[4] });
|
|
for (const m of text.matchAll(BLOB_IMAGE_RE)) push(m, { type: 'blob-image', name: m[1], blob: m[2] });
|
|
for (const m of text.matchAll(BLOB_VIDEO_RE)) push(m, { type: 'blob-video', name: m[1], blob: m[2] });
|
|
for (const m of text.matchAll(BLOB_AUDIO_RE)) push(m, { type: 'blob-audio', name: m[1], blob: m[2] });
|
|
for (const m of text.matchAll(BLOB_PDF_RE)) push(m, { type: 'blob-pdf', name: m[1], blob: m[2] });
|
|
for (const m of text.matchAll(BLOB_TORRENT_RE)) push(m, { type: 'blob-file', name: m[1], blob: m[2] });
|
|
for (const m of text.matchAll(BLOB_FILE_RE)) push(m, { type: 'blob-file', name: m[1], blob: m[2] });
|
|
for (const m of text.matchAll(MD_MENTION_RE)) push(m, { type: 'md-mention', name: m[1], feedId: m[2] });
|
|
for (const m of text.matchAll(RAW_MENTION_RE)) push(m, { type: 'raw-mention', feedId: m[1] });
|
|
for (const m of text.matchAll(BARE_MENTION_RE)) {
|
|
const name = bareMentionName(m[2]);
|
|
if (name) allMatches.push({ index: m.index + m[1].length, length: name.length + 1, type: 'bare-mention', name });
|
|
}
|
|
for (const m of text.matchAll(MSG_REF_RE)) push(m, { type: 'msg-ref', id: m[0] });
|
|
for (const m of text.matchAll(MD_LINK_RE)) push(m, { type: 'md-link', label: m[1], href: m[2] });
|
|
for (const m of text.matchAll(BOLD_RE)) push(m, { type: 'bold', body: m[1] });
|
|
for (const m of text.matchAll(UNDERLINE_RE)) push(m, { type: 'underline', body: m[1] });
|
|
for (const m of text.matchAll(STRIKE_RE)) push(m, { type: 'strike', body: m[1] });
|
|
for (const m of text.matchAll(ITALIC_RE)) push(m, { type: 'italic', body: m[1] });
|
|
for (const m of text.matchAll(INLINE_CODE_RE)) push(m, { type: 'inline-code', body: m[1] });
|
|
for (const m of text.matchAll(URL_RE)) {
|
|
const trimmed = m[0].replace(URL_TAIL_RE, '');
|
|
if (trimmed) allMatches.push({ index: m.index, length: trimmed.length, type: 'url', text: trimmed });
|
|
}
|
|
for (const m of text.matchAll(EMAIL_RE)) push(m, { type: 'email', text: m[0] });
|
|
for (const m of text.matchAll(HASHTAG_RE)) push(m, { type: 'hashtag', tag: m[0].slice(1) });
|
|
allMatches.sort((x, y) => x.index - y.index);
|
|
const filtered = [];
|
|
let lastEnd = 0;
|
|
for (const m of allMatches) {
|
|
if (m.index < lastEnd) continue;
|
|
filtered.push(m);
|
|
lastEnd = m.index + m.length;
|
|
}
|
|
const blobHref = (id) => String(id).startsWith('/blob/sealed/') ? String(id) : `${opts.blobPrefix || '/blob/'}${encodeURIComponent(id)}`;
|
|
const linksOn = opts.links !== false;
|
|
const internalOn = opts.internalLinks !== false;
|
|
const plain = (value) => opts.plainUrlClass ? span({ class: opts.plainUrlClass }, value) : value;
|
|
const inner = (body) => renderStyledText(body, { ...opts, depth: depth + 1 });
|
|
const result = [];
|
|
let cursor = 0;
|
|
for (const m of filtered) {
|
|
if (cursor < m.index) result.push(text.slice(cursor, m.index));
|
|
if (m.type === 'escaped') {
|
|
result.push(m.body);
|
|
} else if (m.type === 'msg-ref') {
|
|
result.push(internalOn ? a({ href: `/thread/${encodeURIComponent(m.id)}`, class: 'styled-link' }, m.id.slice(0, 10) + '...') : m.id);
|
|
} else if (m.type === 'wikilink') {
|
|
result.push(typeof opts.wikiLink === 'function'
|
|
? opts.wikiLink(m.target, slugify(m.target), m.label)
|
|
: internalOn
|
|
? a({ href: `/wiki/${encodeURIComponent(slugify(m.target))}`, class: 'wiki-link' }, m.label)
|
|
: m.label);
|
|
} else if (m.type === 'code-block') {
|
|
result.push(span({ class: 'rt-code-block' }, m.body));
|
|
} else if (m.type === 'rule') {
|
|
result.push(span({ class: 'rt-rule' }));
|
|
} else if (m.type === 'header') {
|
|
result.push(span({ class: `rt-header rt-header-${m.level}` }, ...inner(m.body)));
|
|
} else if (m.type === 'quote') {
|
|
result.push(span({ class: 'rt-quote' }, ...inner(m.body)));
|
|
} else if (m.type === 'bullet') {
|
|
result.push(span({ class: `rt-item rt-item-${m.level}` }, ...inner(m.body)));
|
|
} else if (m.type === 'number') {
|
|
result.push(span({ class: `rt-item rt-item-number rt-item-${m.level}` }, `${m.marker} `, ...inner(m.body)));
|
|
} else if (m.type === 'bold') {
|
|
result.push(strong(...inner(m.body)));
|
|
} else if (m.type === 'underline') {
|
|
result.push(u(...inner(m.body)));
|
|
} else if (m.type === 'strike') {
|
|
result.push(strike(...inner(m.body)));
|
|
} else if (m.type === 'italic') {
|
|
result.push(em(...inner(m.body)));
|
|
} else if (m.type === 'inline-code') {
|
|
result.push(code({ class: 'rt-code' }, m.body));
|
|
} else if (m.type === 'blob-image') {
|
|
const imageSrc = blobHref(m.blob);
|
|
if (opts.zoomImages) {
|
|
zoomSeq = (zoomSeq + 1) % 1000000;
|
|
const zoomId = `rtzoom-${zoomSeq}-${String(m.blob).replace(/[^a-zA-Z0-9]/g, '').slice(-10)}`;
|
|
result.push(a({ href: `#${zoomId}`, id: `${zoomId}-src`, class: 'zoom-link' }, img({ src: imageSrc, alt: m.name || '', class: 'post-image' })));
|
|
result.push(span({ id: zoomId, class: 'lightbox' },
|
|
a({ href: `${imageSrc}${imageSrc.includes('?') ? '&' : '?'}download=1`, class: 'lightbox-download', title: getI18n().fileShareDownload || 'Download' }, '\u2913'),
|
|
a({ href: `#${zoomId}-src`, class: 'lightbox-close' }, '\u00d7'),
|
|
img({ src: imageSrc, alt: m.name || '', class: 'lightbox-image' })
|
|
));
|
|
} else {
|
|
result.push(img({ src: imageSrc, alt: m.name || '', class: 'post-image' }));
|
|
}
|
|
} else if (m.type === 'blob-video') {
|
|
result.push(video({ controls: true, class: 'post-video', src: blobHref(m.blob) }));
|
|
} else if (m.type === 'blob-audio') {
|
|
result.push(audio({ controls: true, class: 'post-audio', src: blobHref(m.blob) }));
|
|
} else if (m.type === 'blob-pdf') {
|
|
const i18n = getI18n();
|
|
const pdfLabel = m.name || i18n.pdfFallbackLabel || 'PDF';
|
|
const clearnet = String(opts.blobPrefix || '').startsWith('/c/');
|
|
let lang = '';
|
|
try { lang = clearnet ? ((require('../models/typed_log').requestScope.getStore() || {}).cnLang || '') : ''; } catch (_) {}
|
|
const fid = require('./pdf_view').frameIdOf(blobHref(m.blob));
|
|
const query = new URLSearchParams({ src: blobHref(m.blob), title: String(m.name || ''), ...(lang ? { lang } : {}), fid });
|
|
const holder = (src, cls) => span({ class: 'pdf-holder' },
|
|
object({ id: fid, data: src, type: 'text/html', class: cls, title: pdfLabel, 'aria-label': pdfLabel }, ''),
|
|
a({ href: '#', class: 'pdf-unmax', title: i18n.pdfClose || 'Close' }, '✕'));
|
|
result.push(clearnet
|
|
? holder(`/c/pdf/embed?${query}`, 'cn-pdf-frame')
|
|
: opts.blobPrefix
|
|
? a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, pdfLabel)
|
|
: holder(`/pdf/embed?${query}`, 'pdf-frame'));
|
|
} else if (m.type === 'blob-file') {
|
|
result.push(a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, m.name || m.blob.slice(1, 9)));
|
|
} else if (m.type === 'md-mention') {
|
|
result.push(internalOn ? a({ href: `/author/${encodeURIComponent('@' + m.feedId)}`, class: 'mention' }, '@' + m.name) : '@' + m.name);
|
|
} else if (m.type === 'raw-mention') {
|
|
result.push(internalOn ? a({ href: `/author/${encodeURIComponent('@' + m.feedId)}`, class: 'mention' }, '@' + m.feedId.slice(0, 8) + '...') : '@' + m.feedId.slice(0, 8) + '...');
|
|
} else if (m.type === 'bare-mention') {
|
|
const feedId = internalOn && mentionResolver ? mentionResolver(m.name) : null;
|
|
result.push(feedId ? a({ href: `/author/${encodeURIComponent(feedId)}`, class: 'mention' }, '@' + m.name) : '@' + m.name);
|
|
} else if (m.type === 'md-link') {
|
|
const external = /^https?:\/\//.test(m.href) && !SELF_HOST_RE.test(m.href);
|
|
result.push(external
|
|
? (linksOn ? a({ href: m.href, class: 'styled-link', target: '_blank', rel: 'noopener noreferrer' }, ...inner(m.label)) : plain(m.label))
|
|
: (internalOn ? a({ href: m.href, class: 'styled-link' }, ...inner(m.label)) : m.label));
|
|
} else if (m.type === 'url') {
|
|
const schemeMatch = m.text.match(/^(https?):\/\//i);
|
|
const href = schemeMatch ? `${schemeMatch[1].toLowerCase()}://${m.text.slice(schemeMatch[0].length)}` : `https://${m.text}`;
|
|
const internalTarget = internalTargetOf(href);
|
|
if (!linksOn) result.push(plain(m.text));
|
|
else if (internalTarget) {
|
|
result.push(internalOn
|
|
? a({ href: internalTarget.href, class: 'internal-link' },
|
|
span({ class: 'internal-link-kind' }, internalTarget.kind.toUpperCase()),
|
|
internalTarget.id ? span({ class: 'internal-link-id' }, shortInternalId(internalTarget.id)) : null
|
|
)
|
|
: plain(internalTarget.href));
|
|
}
|
|
else if (SELF_HOST_RE.test(href)) result.push(a({ href }, m.text));
|
|
else result.push(a({ href, target: '_blank', rel: 'noopener noreferrer' }, m.text));
|
|
} else if (m.type === 'email') {
|
|
result.push(linksOn ? a({ href: `mailto:${m.text}` }, m.text) : plain(m.text));
|
|
} else if (m.type === 'hashtag') {
|
|
const tagHref = typeof opts.hashtagHref === 'function' ? opts.hashtagHref(m.tag) : (internalOn ? `/search?query=%23${encodeURIComponent(m.tag)}` : null);
|
|
result.push(tagHref ? a({ href: tagHref, class: 'tag-link tag-inline' }, `#${m.tag}`) : `#${m.tag}`);
|
|
}
|
|
cursor = m.index + m.length;
|
|
}
|
|
if (cursor < text.length) result.push(text.slice(cursor));
|
|
return result;
|
|
}
|
|
|
|
const safeExternalHref = (url) => {
|
|
const value = String(url || '').trim();
|
|
const lower = value.toLowerCase();
|
|
if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('mailto:')) return value;
|
|
return '';
|
|
};
|
|
|
|
const escapeHtml = (value) => String(value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
|
|
function renderStyledHtml(text, opts = {}) {
|
|
if (text === null || text === undefined || text === '') return '';
|
|
return renderStyledText(String(text), opts)
|
|
.map(node => (node && typeof node === 'object' && node.outerHTML) ? node.outerHTML : escapeHtml(node))
|
|
.join('');
|
|
}
|
|
|
|
function plainText(text) {
|
|
if (text === null || text === undefined) return '';
|
|
return String(text)
|
|
.replace(/```[\s\S]*?```/g, '')
|
|
.replace(/^[ \t]*>[ \t]?/gm, '')
|
|
.replace(/^[ \t]*---[ \t]*$/gm, '')
|
|
.replace(/^[ \t]*#{1,6}[ \t]*/gm, '')
|
|
.replace(/[ \t]*#{1,6}[ \t]*$/gm, '')
|
|
.replace(/^[ \t]*- /gm, '')
|
|
.replace(/^[ \t]*\d{1,3}[.)] /gm, '')
|
|
.replace(/\*\*(.*?)\*\*/g, '$1')
|
|
.replace(/__(.*?)__/g, '$1')
|
|
.replace(/~~(.*?)~~/g, '$1')
|
|
.replace(/\*(.*?)\*/g, '$1')
|
|
.replace(/`([^`]+)`/g, '$1')
|
|
.replace(/!?\[[^\]\n]{0,160}\]\(\s*&[^)\s\n]{1,120}\.sha256\s*\)/g, '')
|
|
.replace(/\[(?:video|audio|pdf):[^\]\n]{0,160}\]\([^)\n]{0,200}\)/g, '')
|
|
.replace(/\[\[([^\]|\n]{1,120})(?:\|([^\]\n]{1,120}))?\]\]/g, (m, target, label) => label || target)
|
|
.replace(/!?\[([^\]\n]{1,160})\]\(([^)\s\n]{0,200})\)/g, (m, label, href) => href ? `${label} (${href})` : label)
|
|
.replace(/\\([\\*_~`#>\-\[\]!])/g, '$1')
|
|
.trim();
|
|
}
|
|
|
|
function renderTextPreview(text, maxLength = 220) {
|
|
const preview = plainText(text).replace(/\n+/g, ' ').trim();
|
|
return preview.length > maxLength ? preview.slice(0, maxLength) + '...' : preview;
|
|
}
|
|
|
|
const FORMAT_MARKS = [
|
|
{ mark: '**bold**', style: 'bold' },
|
|
{ mark: '*italic*', style: 'italic' },
|
|
{ mark: '__underline__', style: 'underline' },
|
|
{ mark: '~~strike~~', style: 'strike' },
|
|
{ mark: '`code`', style: 'code' },
|
|
{ mark: '### title ###', style: 'header' },
|
|
{ mark: '- list', style: 'item' },
|
|
{ mark: '> quote', style: 'quote' },
|
|
{ mark: '[[wiki]]', style: 'wiki' }
|
|
];
|
|
|
|
function renderFormatBar() {
|
|
const i18n = getI18n();
|
|
const marks = [];
|
|
FORMAT_MARKS.forEach((m, i) => {
|
|
if (i) marks.push(span({ class: 'rt-sep' }, '|'));
|
|
marks.push(span({ class: `rt-mark rt-mark-${m.style}` }, m.mark));
|
|
});
|
|
return div({ class: 'rt-toolbar' },
|
|
span({ class: 'rt-toolbar-label' }, `${i18n.formatBarLabel || 'Format'}:`),
|
|
...marks
|
|
);
|
|
}
|
|
|
|
function richTextarea(attrs, ...children) {
|
|
return [renderFormatBar(), textarea(attrs, ...children)];
|
|
}
|
|
|
|
module.exports = { renderStyledText, renderStyledHtml, renderTextPreview, plainText, escapeHtml, safeExternalHref, renderFormatBar, richTextarea, setMentionResolver, INTERNAL_PATHS };
|