mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-28 07:20:49 -04:00
187 lines
7.9 KiB
TypeScript
187 lines
7.9 KiB
TypeScript
import { fetchJson } from './fetchJson.ts';
|
|
import type { Translator } from './translator.ts';
|
|
|
|
let census_groups: Record<string, string> = {};
|
|
let census_comparisons: Record<string, string> = {};
|
|
|
|
const replaceQuoteEntity = (string: string): string => string.replace(/"/g, '"');
|
|
|
|
const mainPlusDetails = (dict: Record<string, string>, wide: boolean) => (_: string, keys: string, content: string) => {
|
|
let selectedDict: Record<string, string> = {};
|
|
if (keys === undefined) {
|
|
selectedDict = dict;
|
|
} else {
|
|
for (const key of keys.substring(1).split(',')) {
|
|
selectedDict[key] = dict[key];
|
|
}
|
|
}
|
|
|
|
return `
|
|
<div class="${wide ? 'wide-escape' : ''}">
|
|
${content.replace(/%group%/g, 'general').replace(/<iframe class="graph" /g, '<iframe class="graph border" ')}${
|
|
Object.keys(selectedDict).map((group) => `
|
|
<details class="border mb-3">
|
|
<summary class="bg-light px-2 py-1" onclick="this.parentElement.querySelector('iframe.graph').contentDocument.location.reload()">${selectedDict[group]}</summary>
|
|
<div class="border-top p-md-3 bg-white">${content.replace(/%group%/g, group)}</div>
|
|
</details>`)
|
|
.join('\n')
|
|
}</div>`;
|
|
};
|
|
|
|
async function replaceAsync(str: string, regex: RegExp, asyncFn: (...args: string[]) => Promise<string>): Promise<string> {
|
|
const promises = [...str.matchAll(regex)].map((match) => asyncFn(...match));
|
|
const data = await Promise.all(promises);
|
|
return str.replace(regex, () => data.shift()!);
|
|
}
|
|
|
|
const generateToC = (content: string, translator: Translator) => (_: string) => {
|
|
const tags = [];
|
|
let curentLevel = 2;
|
|
let needsClosing = false;
|
|
for (const [, levelString, id, title] of content.matchAll(/<h([2-6]) id="([^"]+)">([^<]+)<\/h\1>/g)) {
|
|
const level = parseInt(levelString);
|
|
while (level < curentLevel) {
|
|
tags.push('</li>');
|
|
tags.push('</ul>');
|
|
curentLevel--;
|
|
}
|
|
while (level > curentLevel) {
|
|
tags.push('<ul>');
|
|
curentLevel++;
|
|
needsClosing = false;
|
|
}
|
|
if (needsClosing) {
|
|
tags.push('</li>');
|
|
}
|
|
tags.push('<li>');
|
|
tags.push(`<a href="#${id}">`);
|
|
tags.push(title);
|
|
tags.push('</a>');
|
|
needsClosing = true;
|
|
}
|
|
while (curentLevel < 2) {
|
|
tags.push('</li>');
|
|
tags.push('</ul>');
|
|
curentLevel--;
|
|
needsClosing = false;
|
|
}
|
|
if (needsClosing) {
|
|
tags.push('</li>');
|
|
}
|
|
|
|
return `
|
|
<div class="alert alert-light border">
|
|
<h2 class="h4"><span class="fal fa-list"></span> ${translator.translate('links.blogTOC')}</h2>
|
|
<ul class="mb-0">${tags.join('')}</ul>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
const generateGallery = (_: string, itemsString: string) => {
|
|
const items: Record<string, string> = JSON.parse(`{${replaceQuoteEntity(itemsString).replace(/,\s*$/, '')}}`);
|
|
|
|
const label = (alt: string): string => {
|
|
if (!alt.startsWith('! ')) {
|
|
return '';
|
|
}
|
|
|
|
return `<p class="small mt-2">${alt.substring(2)}</p>`;
|
|
};
|
|
|
|
const cells = Object.entries(items).map(([src, alt]) => `
|
|
<div class="col-6 col-lg-4 columnist-column mb-3">
|
|
<a href="${src}" target="_blank" rel="noopener">
|
|
<img src="${src}" alt="${alt.startsWith('! ') ? alt.substring(2) : alt}">
|
|
</a>
|
|
${label(alt)}
|
|
</div>
|
|
`);
|
|
|
|
return `<div class="row columnist-wall--disabled">${cells.join('')}</div>`;
|
|
};
|
|
|
|
export interface MarkdownInfo {
|
|
title: string | null;
|
|
img: string | null;
|
|
intro: string | null;
|
|
content: string | null;
|
|
}
|
|
|
|
export default async function parseMarkdown(markdown: string, translator: Translator): Promise<MarkdownInfo> {
|
|
let content = `<div>${
|
|
markdown
|
|
.replace(/<table>(.+?)<\/table>/gs, '<div class="table-responsive"><table class="table table-striped small">$1</table></div>')
|
|
.replace(/align="([^"]+)"/g, 'style="text-align:$1"')
|
|
.replace(/<a (href="http[^>]+)>/g, (_match, attributes) => {
|
|
return `<a ${attributes.includes('target=') ? '' : 'target="_blank" '}${attributes.includes('rel=') ? '' : 'rel="noopener" '}${attributes}>`;
|
|
})
|
|
.replace(/<p>{details=(.+?)}<\/p>(.+?)<p>{\/details}<\/p>/gms, '<details class="border mb-3"><summary class="bg-light p-3">$1</summary><div class="border-top p-3 bg-white">$2</div></details>')
|
|
.replace(/<p><img (.*?)><\/p>/g, (_, attrs) => {
|
|
let classNames = 'border';
|
|
const m = attrs.match(/alt="\{(.*)\}/);
|
|
if (m) {
|
|
classNames = m[1];
|
|
attrs = attrs.replace(/alt="{(.*)}/, 'alt="');
|
|
}
|
|
return `<div class="mb-3 text-center"><img ${attrs} class="${classNames}" loading="lazy"></div>`;
|
|
})
|
|
.replace(/{favicon=(.+?)}/g, '<img src="https://$1" alt="Favicon" style="width: 1em; height: 1em;">')
|
|
.replace(/<p>{embed=\/\/(.+?)=(.+?)}<\/p>/g, '<div style="position: relative;height: 0;padding-bottom: 56.25%;"><iframe src="https://$1" title="$2" allowfullscreen sandbox="allow-same-origin allow-scripts allow-popups" style="position: absolute;top: 0; left: 0;width: 100%;height: 100%;border:0;"></iframe></div>')
|
|
.replace(/<p>{graph=([^}]+)}<\/p>/g, '<iframe class="graph" src="$1.html" loading="lazy"></iframe>')
|
|
|
|
.replace(/<p>{set_census_groups=(.+?)}<\/p>/gms, (_, value) => {
|
|
census_groups = JSON.parse(replaceQuoteEntity(value));
|
|
return '';
|
|
})
|
|
.replace(/<p>{set_census_comparisons=(.+?)}<\/p>/gms, (_, value) => {
|
|
census_comparisons = JSON.parse(replaceQuoteEntity(value));
|
|
return '';
|
|
})
|
|
.replace(/<p>{census_groups(:.+?)?}<\/p>(.+?)<p>{\/census_groups}<\/p>/gms, mainPlusDetails(census_groups, false))
|
|
.replace(/<p>{census_comparisons(:.+?)?}<\/p>(.+?)<p>{\/census_comparisons}<\/p>/gms, mainPlusDetails(census_comparisons, true))
|
|
.replace(/<h1 id="🏳️🌈-/g, '<h1 id="') // license header
|
|
.replace(/ id=""/g, '')
|
|
.replace(/<p>{wide_table}<\/p>/g, '<div class="table-wide table-responsive my-5 headers-nowrap">')
|
|
.replace(/<p>{\/wide_table}<\/p>/g, '</div>')
|
|
.replace(/<p>{gallery={(.*?)}}<\/p>/gms, generateGallery)
|
|
.replace(/(?<![\w!])!!([^<>]*?)!!(?![\w!])/g, (_: string, spoilerString: string): string => {
|
|
return `<span class="spoiler">${spoilerString}</span>`;
|
|
})
|
|
}</div>`;
|
|
|
|
content = await replaceAsync(content, /{json=([^=}]+)=([^=}]+)}/g, async (_, filename, key) => {
|
|
try {
|
|
return await fetchJson(translator.config.locale, filename, key);
|
|
} catch (error) {
|
|
return `<span class="badge bg-danger text-white">${error}</span>`;
|
|
}
|
|
});
|
|
|
|
content = content.replace(/<p>{table_of_contents}<\/p>/g, generateToC(content, translator));
|
|
|
|
content = content.replace(/{optional}(.+?){\/optional}/gms, (_, content) => {
|
|
if (content.includes('badge bg-danger')) {
|
|
return '';
|
|
}
|
|
return content;
|
|
});
|
|
|
|
const titleMatch = content.match('<h1[^>]*>(.+?)</h1>');
|
|
const title = titleMatch ? replaceQuoteEntity(titleMatch[1]) : null;
|
|
const imgMatch = content.match('<img src="([^"]+)"[^>]*>');
|
|
const img = imgMatch ? imgMatch[1] : null;
|
|
let intro: string[] = [];
|
|
|
|
for (const introMatch of content.matchAll(/<p[^>]*>(.+?)<\/p>/gms)) {
|
|
const p = introMatch[1].replace(/(<([^>]+)>)/ig, '').replace(/\s+/g, ' ');
|
|
intro = [...intro, ...p.split(' ')];
|
|
}
|
|
|
|
return {
|
|
title,
|
|
img,
|
|
intro: intro.length ? intro.slice(0, 24).join(' ') : null,
|
|
content,
|
|
};
|
|
}
|