@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
350 lines (313 loc) • 12.4 kB
JavaScript
import { marked } from 'marked';
import { FileService } from '../../services/file/file.service.js';
import { append, getBlobFromUint8ArrayFile, getRawContentFile, htmls, s, sa } from './VanillaJs.js';
import { s4 } from './CommonJs.js';
import { Translate } from './Translate.js';
import { Modal, renderViewTitle } from './Modal.js';
import { DocumentService } from '../../services/document/document.service.js';
import { CoreService, getApiBaseUrl, headersFactory } from '../../services/core/core.service.js';
import { loggerFactory } from './Logger.js';
import { imageShimmer, renderChessPattern, renderCssAttr, styleFactory } from './Css.js';
import { getQueryParams, setPath } from './Router.js';
const logger = loggerFactory(import.meta);
const attachMarkdownLinkHandlers = (containerSelector) => {
const container = s(containerSelector);
if (!container || container.dataset.mdLinkHandler) return;
container.dataset.mdLinkHandler = 'true';
container.addEventListener('click', async (e) => {
const link = e.target.closest('.markdown-content a[href]');
if (!link) return;
const href = link.getAttribute('href');
if (!href || href.startsWith('#')) return;
e.preventDefault();
const isExternal = href.startsWith('http://') || href.startsWith('https://');
if (isExternal) {
const result = await Modal.RenderConfirm({
id: `external-link-${s4()}`,
html: async () => html`
<div class="in section-mp" style="text-align: center; padding: 20px;">
<p>${Translate.instance('external-link-warning')}</p>
<p style="word-break: break-all; margin-top: 10px;"><strong>${href}</strong></p>
</div>
`,
icon: html`<i class="fas fa-external-link-alt"></i>`,
style: {
width: '350px',
height: '500px',
overflow: 'auto',
'z-index': '11',
resize: 'none',
},
});
if (result && result.status === 'confirm') {
window.open(href, '_blank', 'noopener,noreferrer');
}
} else {
setPath(href);
}
});
};
class Content {
static async instance(options = { idModal: '', titleIcon: '' }) {
const { idModal } = options;
setTimeout(async () => {
try {
Modal.Data[idModal].onObserverListener[`main-content-observer`] = () => {
const height = s(`.${idModal}`).offsetHeight - Modal.headerTitleHeight - 1;
if (s(`.iframe-${idModal}`)) s(`.iframe-${idModal}`).style.height = `${height}px`;
if (s(`.ssr-shimmer-content-${idModal}`)) s(`.ssr-shimmer-content-${idModal}`).style.height = `${height}px`;
};
Modal.Data[idModal].onObserverListener[`main-content-observer`]();
s(`.error-${idModal}`).classList.add('hide');
s(`.ssr-shimmer-content-${idModal}`).classList.remove('hide');
const queryParams = getQueryParams();
let documentObj, file, md;
if (!queryParams.cid) throw new Error(`no-result-found`);
{
const { data: responseData, status, message } = await DocumentService.get({ id: queryParams.cid });
const data = Array.isArray(responseData) ? responseData : responseData?.data || [];
if (status !== 'success' || !data || !data[0]) {
logger.error(message);
throw new Error(`no-result-found`);
}
documentObj = data[0];
}
// Get file metadata (does not include buffer data)
if (documentObj.fileId && documentObj.fileId._id) {
const { data, status, message } = await FileService.get({ id: documentObj.fileId._id });
if (status !== 'success' || !data || !data[0]) {
logger.warn('File metadata not found:', message);
// Continue without file - not fatal
} else {
file = data[0];
}
}
// Get markdown file metadata (optional)
if (documentObj.mdFileId && documentObj.mdFileId._id) {
const { data, status, message } = await FileService.get({ id: documentObj.mdFileId._id });
if (status !== 'success' || !data || !data[0]) {
logger.warn('Markdown file metadata not found:', message);
// Continue without markdown - try to render file instead
} else {
md = data[0];
}
}
// Check if we have at least one file to render
if (!md && !file) {
throw new Error(`no-preview-available`);
}
// Use custom titleIcon from options, or extract from the modal's original title HTML, or fall back to default
const titleIcon = options.titleIcon
? options.titleIcon
: Modal.Data[idModal] &&
Modal.Data[idModal].options &&
Modal.Data[idModal].options.title &&
Modal.Data[idModal].options.title.includes &&
Modal.Data[idModal].options.title.includes('<img')
? Modal.Data[idModal].options.title.match(/<img[^>]*>/)?.[0] || html`<i class="inl far fa-file"></i>`
: html`<i class="inl far fa-file"></i>`;
// Preserve the original text wrapper class if present in the modal's stored title
const originalTitle = Modal.Data[idModal]?.options?.title || '';
const hasCustomTextClass = originalTitle.includes && originalTitle.includes('underpost-text-title-modal');
const docTitle = documentObj.title ? documentObj.title : documentObj.location;
const titleText = hasCustomTextClass
? `<span class='inl underpost-text-title-modal'>${docTitle}</span>`
: docTitle;
htmls(
`.title-modal-${idModal}`,
html`${renderViewTitle({
icon: titleIcon,
text: titleText,
})} `,
);
htmls(`.content-render-${idModal}`, ``);
// Pass file IDs to RenderFile - it will fetch blobs as needed
if (md) await Content.RenderFile({ idModal, file: md, id: md._id });
if (file) await Content.RenderFile({ idModal, file, id: file._id });
Modal.Data[idModal].onObserverListener[`main-content-observer`]();
} catch (error) {
htmls(`.content-render-${idModal}`, '');
htmls(
`.error-${idModal}`,
html`<i class="fas fa-exclamation-circle"></i> ${Translate.instance(error.message)}`,
);
s(`.error-${idModal}`).classList.remove('hide');
}
s(`.ssr-shimmer-content-${idModal}`).classList.add('hide');
});
return html` <div class="in content-render content-render-${idModal}" style="min-height: 200px"></div>
<div
class="abs ssr-shimmer-content-${idModal}"
style="${renderCssAttr({
style: {
top: '0px',
left: '0px',
width: '100%',
},
})}"
>
${imageShimmer()}
</div>
<div class="abs center error-${idModal} hide"></div>`;
}
/**
* Helper function to get file content
* Supports both legacy format (with buffer data) and new format (metadata only)
* For new format, fetches content from blob endpoint
*/
static async getFileContent(file, options = {}) {
// If custom URL provided, use it
if (options.url) {
return await CoreService.getRaw({ url: options.url });
}
// If buffer data exists in file object (legacy format), use it
if (file.data?.data) {
return await getRawContentFile(getBlobFromUint8ArrayFile(file.data.data, file.mimetype));
}
// Otherwise, fetch from blob endpoint (new metadata-only format)
if (file._id) {
try {
const { data: blobArray, status } = await FileService.get({ id: `blob/${file._id}` });
if (status === 'success' && blobArray && blobArray[0]) {
return await blobArray[0].text();
}
throw new Error('Failed to fetch file content');
} catch (error) {
logger.error('Error fetching file content from blob endpoint:', error);
throw new Error('Could not load file content');
}
}
throw new Error('No file content available');
}
static async RenderFile(
options = {
file: {
_id: '',
mimetype: '',
url: '',
name: '',
cid: '',
},
idModal: '',
style: {},
class: '',
container: '',
url: '',
raw: false,
},
) {
if (!options.container) options.container = `.content-render-${options.idModal}`;
if (!options.style)
options.style = {
width: '100%',
border: 'none',
};
if (!options.class) options.class = ``;
const { container, file } = options;
const ext = (file.name || '').split('.').pop()?.toLowerCase() || '';
let render = '';
try {
switch (ext) {
case 'md':
{
const content = await Content.getFileContent(file, options);
render += html`<div class="${options.class} markdown-content" ${styleFactory(options.style)}>
${marked.parse(content)}
</div>`;
}
break;
case 'jpg':
case 'jpeg':
case 'webp':
case 'svg':
case 'gif':
case 'png': {
const url = await Content.urlFactory(options);
if (url) {
const imgRender = html`<img
alt="${file.name ? file.name : `file ${s4()}`}"
class="in ${options.class}"
${styleFactory(options.style, `${renderChessPattern(50)}`)}
src="${url}"
/>`;
render += imgRender;
} else {
render = html`<div class="in ${options.class}" ${styleFactory(options.style)}>
<p style="color: red;">Error loading image</p>
</div>`;
}
break;
}
case 'pdf': {
const url = await Content.urlFactory(options);
if (url) {
render += html`<iframe
class="in ${options.class} iframe-${options.idModal}"
${styleFactory(options.style)}
src="${url}"
></iframe>`;
} else {
render = html`<div class="in ${options.class}" ${styleFactory(options.style)}>
<p style="color: red;">Error loading PDF</p>
</div>`;
}
break;
}
case 'json':
{
const content = await Content.getFileContent(file, options);
render += html`<pre class="in ${options.class}" ${styleFactory(options.style)}>
${JSON.stringify(JSON.parse(content), null, 4)}</pre
>`;
}
break;
default:
{
const content = await Content.getFileContent(file, options);
render += html`<div class="in ${options.class}" ${styleFactory(options.style)}>${content}</div>`;
}
break;
}
} catch (error) {
logger.error('Error rendering file:', error);
render = html`<div class="in ${options.class}" ${styleFactory(options.style)}>
<p style="color: red;">Error loading file: ${error.message}</p>
</div>`;
}
if (options.raw) return render;
append(container, render);
// Scrape and handle markdown links after DOM insertion
if (ext === 'md') {
attachMarkdownLinkHandlers(container);
}
}
/**
* Generate appropriate URL for file display
* Prefers blob endpoint for new metadata-only format
*/
static async urlFactory(options) {
// If custom URL provided, use it
if (options.url) {
return options.url;
}
// If buffer data exists (legacy), create object URL
if (options.file?.data?.data) {
return URL.createObjectURL(getBlobFromUint8ArrayFile(options.file.data.data, options.file.mimetype));
}
// Use blob endpoint for metadata-only format with proper authentication
if (options.file?._id) {
try {
const { data: blobArray, status } = await FileService.get({ id: `blob/${options.file._id}` });
if (status === 'success' && blobArray && blobArray[0]) {
return URL.createObjectURL(blobArray[0]);
}
throw new Error('Failed to fetch file blob');
} catch (error) {
logger.error('Error fetching file blob:', error);
return null;
}
}
return null;
}
}
export { Content, attachMarkdownLinkHandlers };