///
// Import necessary libraries
import { marked } from 'marked';
import kebabCase from 'lodash/kebabCase';
const { extractStars } = require('../helper/extract-stars.function.js');
const IsBot = require('../app/modules/web/util/is-bot.js');
import hljs from 'highlight.js/lib/core';
// Import languages using standard import syntax
import nginx from 'highlight.js/lib/languages/nginx';
import xml from 'highlight.js/lib/languages/xml';
import css from 'highlight.js/lib/languages/css';
import scss from 'highlight.js/lib/languages/scss';
import yaml from 'highlight.js/lib/languages/yaml';
import powershell from 'highlight.js/lib/languages/powershell';
import javascript from 'highlight.js/lib/languages/javascript';
import json from 'highlight.js/lib/languages/json';
import shell from 'highlight.js/lib/languages/shell';
import typescript from 'highlight.js/lib/languages/typescript';
import ini from 'highlight.js/lib/languages/ini';
// Register languages with highlight.js
hljs.registerLanguage('conf', nginx);
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('css', css);
hljs.registerLanguage('scss', scss);
hljs.registerLanguage('yaml', yaml);
hljs.registerLanguage('yml', yaml);
hljs.registerLanguage('powershell', powershell);
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('js', javascript);
hljs.registerLanguage('json', json);
hljs.registerLanguage('bash', shell);
hljs.registerLanguage('sh', shell);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('ts', typescript);
hljs.registerLanguage('ini', ini);
// Initialize the custom renderer
const markdownRenderer = new marked.Renderer();
// Initialize counters and variables
let codeIndex: number = 0;
let currentRepo: any;
let settings: any;
let currentRepoPath: any;
let locationOrigin: any;
let locationPathname: any;
let locationHref: any;
let locationHostname: any;
// Helper function to strip HTML tags
function htmlStrip(html: string): string {
return html.replace(/<\/?[^>]+(>|$)/g, "");
}
// Helper function to sanitize strings to prevent XSS
const sanitize = (str: any): string => {
if (typeof str !== 'string') return '';
return str.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(//g, '>');
};
// Helper function to notify missing markdown code
const notifyMissingMarkdownCode = ({ code, language, currentRepo, currentRepoPath, coreUrl }: any): void => {
const url = `${coreUrl}/api/patrikx3/git/notify-markdown-error`;
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
info: `
Repo: ${sanitize(currentRepo)}
Path: ${sanitize(currentRepoPath)}
Code:
${sanitize(code)}
Language: ${sanitize(language)}
`
})
}).then((response: any) => {
if (response.status !== 200) {
console.error('notifyMissingMarkdownCode invalid response', response);
} else {
console.info('notifyMissingMarkdownCode info response', response);
}
}).catch((e: any) => {
console.error('notifyMissingMarkdownCode error', e);
});
};
// Helper function to render images from tokens
const renderImages = (tokens: any[]): string => {
return tokens.map((childToken: any) => {
if (childToken.type === 'image') {
const imgTitle = childToken.title ? ` title="${sanitize(childToken.title)}"` : '';
const altText = sanitize(childToken.text);
//console.log('childToken', childToken.href)
if (childToken.href === 'https://snapcraft.io/static/images/badges/en/snap-store-black.svg') {
childToken.href = 'https://cdn.corifeus.com/assets/svg/snap-store-black.svg'
}
return `
`;
}
// Handle other child token types if necessary
return '';
}).join('');
};
// Custom heading renderer
markdownRenderer.heading = (token: any): string => {
const text: string = token.text;
const level: number = token.depth;
const raw: string = token.raw;
const ref: string = kebabCase(htmlStrip(raw)).replace(/[^\x00-\xFF]/g, "");
const id: string = `${ref}-parent`;
let navClick: string = '';
if (!IsBot()) {
navClick = `onclick="return window.coryAppWebPagesNavigateHash('${sanitize(id)}');"`;
}
const element: string = ``;
return element;
};
// Custom image renderer
markdownRenderer.image = (token: any): string => {
let { href, title, text } = token;
title = title || '';
text = text || '';
if (!href.startsWith('http')) {
href = `https://cdn.corifeus.com/git/${sanitize(currentRepo)}/${sanitize(href)}`;
}
let result: string;
if (text.toLowerCase().trim() === 'link') {
result = `
`;
} else {
result = `
${sanitize(title)}
${sanitize(text)}
`;
}
return result;
};
// Custom link renderer
markdownRenderer.link = (token: any): string => {
const title: any = token.title;
let href: any = token.href;
const text: any = token.text;
let a: string;
let tooltip: string = '';
if (title !== null && title !== undefined) {
tooltip = `tooltip="${sanitize(title)}"`;
}
let fixed: boolean = false;
let path: any;
const testHref: any = href.toLowerCase();
const fixedUrl = (): void => {
try {
const url = new URL(href);
href = url.pathname;
path = `${href}`;
fixed = true;
} catch (error: any) {
console.error('Invalid URL:', href, error);
}
};
if (typeof testHref === 'string' && (testHref.startsWith('https://') || testHref.startsWith('http://'))) {
try {
const testUrl = new URL(testHref);
for (let defaultDomain of settings.pages.defaultDomain) {
if (testUrl.hostname === defaultDomain) {
fixedUrl();
break;
}
}
} catch (error: any) {
console.error('Invalid testHref URL:', testHref, error);
}
} else if (testHref.includes('localhost:8080')) {
fixedUrl();
}
// Detect if the link contains image tokens
const hasImage: boolean = token.tokens && token.tokens.some((childToken: any) => childToken.type === 'image');
// Determine if the link is external
const isExternal: boolean = (
!href.startsWith(locationOrigin) &&
(href.startsWith('https://') || href.startsWith('http://') || href.startsWith('mailto:'))
);
if (isExternal) {
if (href.endsWith('#cory-non-external')) {
if (hasImage) {
// Render image inside link without external link icon
a = `${renderImages(token.tokens)}`;
} else {
a = `${sanitize(text)}`;
}
} else {
if (hasImage) {
// Render image inside link with external link icon
a = `${renderImages(token.tokens)} `;
} else {
a = `${sanitize(text)} `;
}
}
} else {
if (!fixed) {
if (href.endsWith('.md')) {
href = href.substr(0, href.length - 3) + '.html';
}
if (href.startsWith(locationOrigin)) {
path = `/${href.substring(locationOrigin.length + 1)}`;
} else if (href.startsWith('./')) {
let base = locationHref;
if (!base.includes('.')) {
base = locationHref + '/';
}
try {
path = `${new URL(href, base).pathname}`;
} catch (error: any) {
console.error('Invalid relative URL:', href, error);
path = href; // Fallback to original href
}
} else {
path = `/${sanitize(currentRepo)}/${sanitize(href)}`;
}
}
const navClick: string = !IsBot() ? `onclick="window.coryAppWebPagesNavigate('${sanitize(path)}'); return false;"` : '';
if (hasImage) {
// Render image inside link
a = `${renderImages(token.tokens)}`;
} else {
// Render link with text
a = `${sanitize(text)}`;
}
}
return a;
};
// Custom code block renderer
markdownRenderer.code = (token: any): string => {
// code, language
const code: string = token.text;
let language: any = token.lang;
if (language === undefined) {
language = 'text';
}
language = language.toLowerCase();
if ((hljs.getLanguage(language) === 'undefined' || hljs.getLanguage(language) === undefined) && language !== 'text' && language !== 'txt') {
console.error(`Please add highlight.js as a language (could be a marked error as well, sometimes it thinks a language): ${language}
We are not loading everything, since it is about 500kb`);
notifyMissingMarkdownCode({
code,
language,
currentRepo,
currentRepoPath,
coreUrl: settings.core.server.url
});
}
// language = language === 'text' || language === 'txt' || language === undefined ? 'html' : language;
const validLang: boolean = !!(language && hljs.getLanguage(language));
const highlighted: string = validLang ? hljs.highlight(code, {
language: language as string
}).value : sanitize(code);
codeIndex++;
return ``;
};
// Custom inline code renderer
markdownRenderer.codespan = (token: any): string => {
//console.warn('codespan', token)
const code: string = token.text;
const lang: string = 'html';
const highlighted: string = hljs.highlight(code, {
language: lang,
}).value;
return `${highlighted}`;
};
// Customize the rendering of tables
markdownRenderer.table = function (token: any): string {
console.log('table', token)
const header = token.header.map((cell: any) => this.tablecell(cell)).join('');
const rows = token.rows
.map((row: any[]) => this.tablerow({ text: row.map((cell: any) => this.tablecell(cell)).join('') }))
.join('');
return ``;
};
// Customize the rendering of table rows
markdownRenderer.tablerow = function (token: any): string {
return `${token.text}
`;
};
// Customize the rendering of table cells
markdownRenderer.tablecell = function (token: any): string {
const tag = token.header ? 'th' : 'td';
const alignment = token.header ? ` style="text-align: ${token.header ? 'center' : 'left'};"` : '';
// Parse markdown content in cells
const content = marked.parseInline(token.text);
return `<${tag} class="corifeus-marked-table-cell"${alignment}>${content}${tag}>`;
};
// Helper function to extract specific sections from Markdown
const extract = (template: any, area: any): string => {
// [//]: #@corifeus-header
// [//]: #corifeus-header:end
// [//]: #@corifeus-footer
// [//]: #@corifeus-footer:end
const start: string = `[//]: #@${area}`;
const end: string = `[//]: #@${area}:end`;
const startIndex: number = template.indexOf(start);
const endIndex: number = template.indexOf(end);
// If start or end markers are not found, return the original template
if (startIndex === -1 || endIndex === -1) {
return template;
}
let result: string = template.substring(0, startIndex + start.length);
result += template.substring(endIndex + end.length);
return result;
};
// Helper function to escape regex special characters
const RegexpEscape = (s: string): string => {
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
};
// Construct function to process Markdown and convert to HTML
const construct = (data: any): string => {
currentRepo = data.currentRepo;
settings = data.settings;
//console.log('data', data)
locationOrigin = data.location.origin;
currentRepoPath = data.path;
locationPathname = data.location.pathname;
locationHref = data.location.href;
locationHostname = data.location.hostname;
let { md, packages, path } = data;
md = md.trim();
md = extract(md, 'corifeus-header');
md = extract(md, 'corifeus-footer');
/*
md = twemoji.parse(md, {
folder: 'svg',
ext: '.svg',
})
*/
let html: string = marked(md, {
renderer: markdownRenderer
});
html = html.replace(/{/g, '{').replace(/}/g, '}');
html = html.replace(/&/g, '&');
if (currentRepo === 'corifeus' && path === 'index.html') {
//console.info('decorated corifeus index.html')
for (let pkgName of Object.keys(packages)) {
const pkg: any = packages[pkgName];
//console.log(pkgName, pkg.corifeus.stargazers_count)
//if (pkg.corifeus.stargazers_count > -1) {
// Construct the hidden star placeholder dynamically
const hiddenStars: string = `<!--@star|${pkg.name}-->`;
let title: string = '';
// Handle large star counts by adding a title
if (pkg.corifeus.stargazers_count > 999) {
title = pkg.corifeus.stargazers_count.toString();
}
let stars: string = '';
if (pkg.corifeus.stargazers_count > 0) {
// Construct the star count HTML element
stars = ` ${extractStars(pkg.corifeus.stargazers_count)}`;
}
// Escape the placeholder string so it can be safely used in the regex
const re: RegExp = new RegExp(RegexpEscape(hiddenStars), 'g');
// Perform the replacement
html = html.replace(re, stars);
//}
}
}
return html;
};
// Web Worker message handling
onmessage = function (e: MessageEvent): void {
const data: any = {
requestId: e.data.requestId
}
try {
data.html = construct(e.data);
data.success = true;
} catch (error: any) {
console.error(error);
data.success = false;
data.errorMessage = error.message;
}
postMessage(data);
}