chore: update Angular and TypeScript type definitions in cache
This commit is contained in:
+254
-256
@@ -1,8 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import hljs from 'highlight.js';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import markdownItAnchor from 'markdown-it-anchor';
|
||||
import markdownItTaskLists from 'markdown-it-task-lists';
|
||||
import markdownItAttrs from 'markdown-it-attrs';
|
||||
import { Note } from '../types';
|
||||
|
||||
type TableAlignment = 'left' | 'center' | 'right' | null;
|
||||
interface MarkdownRenderEnv {
|
||||
codeBlockIndex: number;
|
||||
}
|
||||
|
||||
interface MarkdownItToken {
|
||||
info?: string;
|
||||
content: string;
|
||||
tag: string;
|
||||
attrGet(name: string): string | null;
|
||||
attrSet(name: string, value: string): void;
|
||||
attrJoin(name: string, value: string): void;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -13,158 +28,15 @@ export class MarkdownService {
|
||||
private tagColorCache = new Map<string, number>();
|
||||
|
||||
render(markdown: string, allNotes: Note[], currentNote?: Note): string {
|
||||
let html = markdown;
|
||||
const env: MarkdownRenderEnv = { codeBlockIndex: 0 };
|
||||
const markdownIt = this.createMarkdownIt(env);
|
||||
|
||||
// Process code blocks first to prevent inner content from being parsed
|
||||
const codeBlocks = new Map<string, { html: string; language: string }>();
|
||||
html = html.replace(/```([^\n]*)\n([\s\S]*?)```/g, (match, lang, code) => {
|
||||
const id = `___CODEBLOCK_${codeBlocks.size}___`;
|
||||
const rawLanguage = (lang ?? '').trim();
|
||||
const normalizedLanguage = rawLanguage.toLowerCase();
|
||||
const isMermaid = normalizedLanguage === 'mermaid';
|
||||
let highlightedCode = '';
|
||||
const preprocessed = this.decorateInlineTags(markdown);
|
||||
let html = markdownIt.render(preprocessed, env);
|
||||
|
||||
if (!isMermaid) {
|
||||
if (normalizedLanguage && hljs.getLanguage(normalizedLanguage)) {
|
||||
highlightedCode = hljs.highlight(code, { language: normalizedLanguage }).value;
|
||||
} else if (normalizedLanguage) {
|
||||
highlightedCode = hljs.highlightAuto(code).value;
|
||||
} else {
|
||||
highlightedCode = hljs.highlightAuto(code).value;
|
||||
}
|
||||
}
|
||||
|
||||
const languageLabel = rawLanguage || 'plaintext';
|
||||
const languageDisplay = rawLanguage ? rawLanguage : 'Plain text';
|
||||
const encodedRawCode = encodeURIComponent(code);
|
||||
const safeLanguageDisplay = this.escapeHtml(languageDisplay);
|
||||
const headerHtml = `
|
||||
<div class="code-block__header">
|
||||
<div class="code-block__controls" aria-hidden="true">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="code-block__actions">
|
||||
<button type="button" class="code-block__language-badge" data-language="${languageLabel.toLowerCase()}" data-code-id="${id}">${safeLanguageDisplay}</button>
|
||||
<span class="code-block__copy-feedback" hidden></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const codeHtml = isMermaid
|
||||
? `
|
||||
<div class="code-block__body mermaid-block__body">
|
||||
<div class="mermaid-diagram" data-mermaid-code="${encodedRawCode}"></div>
|
||||
<pre class="mermaid-source" hidden><code class="hljs" data-raw-code="${encodedRawCode}">${this.escapeHtml(code)}</code></pre>
|
||||
</div>
|
||||
`
|
||||
: `<pre class="code-block__body"><code class="hljs" data-raw-code="${encodedRawCode}">${highlightedCode}</code></pre>`;
|
||||
const wrapperClass = isMermaid ? 'code-block code-block--mermaid text-left' : 'code-block text-left';
|
||||
const wrapper = `<div class="${wrapperClass}" style="max-width: 800px; width: 100%; margin: 0;" data-language="${languageLabel.toLowerCase()}" data-code-id="${id}">${headerHtml}${codeHtml}</div>`;
|
||||
|
||||
codeBlocks.set(id, { html: wrapper, language: normalizedLanguage });
|
||||
return id;
|
||||
});
|
||||
html = this.convertTables(html);
|
||||
html = this.decorateInlineTags(html);
|
||||
|
||||
// Headings (H1-H6)
|
||||
let firstLevelOneHeadingSkipped = false;
|
||||
html = html.replace(/^(\#{1,6})\s+(.*)/gm, (match, hashes, content) => {
|
||||
const level = hashes.length;
|
||||
if (level === 1 && !firstLevelOneHeadingSkipped) {
|
||||
firstLevelOneHeadingSkipped = true;
|
||||
return '';
|
||||
}
|
||||
const id = this.slugify(content);
|
||||
const headingClass = `md-heading md-heading-${level}`;
|
||||
return `<h${level} id="${id}" class="${headingClass} text-obs-l-text-main dark:text-white font-bold mt-6 mb-3 pb-1 border-b border-obs-l-border dark:border-obs-d-border">${content}</h${level}>`;
|
||||
});
|
||||
|
||||
// Bold, Italic, Strikethrough
|
||||
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
html = html.replace(/~~(.*?)~~/g, '<del>$1</del>');
|
||||
|
||||
// Blockquotes & Callouts
|
||||
html = html.replace(/^>\s?\[!(\w+)\]\n(>.*)/gm, (match, type, content) => {
|
||||
const calloutType = type.toUpperCase();
|
||||
const calloutClass = this.getCalloutClass(calloutType);
|
||||
const cleanedContent = content.replace(/^>\s?/gm, '');
|
||||
const title = this.formatCalloutTitle(calloutType);
|
||||
const bodyHtml = cleanedContent
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
.map(line => `<p>${line}</p>`)
|
||||
.join('');
|
||||
|
||||
return `<div class="${calloutClass} text-left" style="max-width: 800px; width: 100%; margin: 0;" data-callout-type="${calloutType.toLowerCase()}"><div class="callout__title">${title}</div><div class="callout__body">${bodyHtml}</div></div>`;
|
||||
});
|
||||
html = html.replace(/^>\s(.*)/gm, '<blockquote class="border-l-4 border-gray-300 dark:border-gray-500 pl-4 py-2 my-4 italic text-gray-700 dark:text-gray-300">$1</blockquote>');
|
||||
|
||||
// Task lists
|
||||
const transformTaskItem = (_match: string, marker: string, rawContent: string) => {
|
||||
const isChecked = marker.toLowerCase() === 'x';
|
||||
const classes = [
|
||||
'md-task-item',
|
||||
'text-obs-l-text-main',
|
||||
'dark:text-obs-d-text-main'
|
||||
];
|
||||
if (isChecked) {
|
||||
classes.push('md-task-item--done');
|
||||
}
|
||||
const content = rawContent.trim();
|
||||
const checkboxState = isChecked ? 'checked' : '';
|
||||
const ariaChecked = isChecked ? 'true' : 'false';
|
||||
return `<li class="${classes.join(' ')}"><label class="md-task"><input type="checkbox" class="md-task-checkbox" ${checkboxState} disabled aria-checked="${ariaChecked}" tabindex="-1"><span class="md-task-label-text">${content}</span></label></li>`;
|
||||
};
|
||||
|
||||
html = html.replace(/^\s*(?:[\-\*]|\d+\.)\s+\[( |x|X)\]\s+(.*)$/gm, transformTaskItem);
|
||||
|
||||
// Lists (simple implementation)
|
||||
html = html.replace(/^\s*[\-\*]\s(.*)/gm, '<li class="ml-6 list-disc">$1</li>');
|
||||
html = html.replace(/^\s*\d+\.\s(.*)/gm, '<li class="ml-6 list-decimal">$1</li>');
|
||||
html = html.replace(/<\/li>\n<li/g, '</li><li'); // Fix spacing
|
||||
html = html.replace(/(<li.*<\/li>)/gs, (match) => {
|
||||
if (match.includes('md-task-item')) return `<ul class="mb-4 md-task-list">${match}</ul>`;
|
||||
if (match.includes('list-disc')) return `<ul class="mb-4">${match}</ul>`;
|
||||
if (match.includes('list-decimal')) return `<ol class="mb-4">${match}</ol>`;
|
||||
return match;
|
||||
});
|
||||
|
||||
// Inline Code
|
||||
html = html.replace(/(`+)([\s\S]*?)\1/g, (match, fence, inlineCode) => {
|
||||
if (inlineCode.includes('\n')) {
|
||||
return match;
|
||||
}
|
||||
const safeContent = this.escapeHtml(inlineCode.trim());
|
||||
return `<code class="inline-code">${safeContent}</code>`;
|
||||
});
|
||||
|
||||
// External images 
|
||||
html = html.replace(/!\[(.*?)\]\(((?:https?|ftp|file):\/\/[^\s\)]*)(?:\s+"(.*?)")?\)/g, (match, alt, url, title) => {
|
||||
const safeAlt = this.escapeHtml(alt ?? '');
|
||||
const safeUrl = this.escapeHtml(url ?? '');
|
||||
const safeTitle = title ? this.escapeHtml(title) : '';
|
||||
const titleAttr = title ? ` title="${safeTitle}"` : '';
|
||||
const captionSource = title && title.trim() ? title : alt;
|
||||
const safeCaption = captionSource ? this.escapeHtml(captionSource) : '';
|
||||
const figcaption = safeCaption
|
||||
? `<figcaption class="text-center text-sm text-obs-l-text-muted dark:text-obs-d-text-muted">${safeCaption}</figcaption>`
|
||||
: '';
|
||||
return `<figure class="my-4 md-attachment-figure">
|
||||
<img src="${safeUrl}" alt="${safeAlt}"${titleAttr} loading="lazy" class="rounded-lg max-w-full h-auto mx-auto">
|
||||
${figcaption}
|
||||
</figure>`;
|
||||
});
|
||||
|
||||
// External links [text](url "title")
|
||||
html = html.replace(/\[(.*?)\]\(((?:https?|ftp|file|mailto):\/\/[^\s\)]*)(?:\s+"(.*?)")?\)/g, (match, label, url, title) => {
|
||||
const safeLabel = this.escapeHtml(label ?? '');
|
||||
const safeUrl = this.escapeHtml(url ?? '');
|
||||
const safeTitle = title ? this.escapeHtml(title) : '';
|
||||
const titleAttr = title ? ` title="${safeTitle}"` : '';
|
||||
return `<a href="${safeUrl}"${titleAttr} target="_blank" rel="noopener noreferrer" class="md-external-link">${safeLabel}</a>`;
|
||||
});
|
||||
html = this.transformCallouts(html);
|
||||
html = this.transformTaskLists(html);
|
||||
html = this.wrapTables(html);
|
||||
|
||||
// Embedded Files ![[file.png]] using Attachements-Path from HOME.md (traité avant les liens internes)
|
||||
const homeNote = allNotes.find(n =>
|
||||
@@ -190,16 +62,6 @@ export class MarkdownService {
|
||||
</figure>`;
|
||||
});
|
||||
|
||||
// Horizontal Rule
|
||||
html = html.replace(/^-{3,}/gm, '<hr class="my-6 border-obs-l-border dark:border-obs-d-border">');
|
||||
// Paragraphs and line breaks
|
||||
html = html.split('\n').map(p => p.trim() === '' ? '' : `<p>${p}</p>`).join('').replace(/<\/p><p>/g, '</p><br><p>');
|
||||
|
||||
// Restore code blocks
|
||||
for (const [id, block] of codeBlocks.entries()) {
|
||||
html = html.replace(id, block.html);
|
||||
}
|
||||
|
||||
// Placeholder for unsupported elements
|
||||
html = html.replace(/\$\$(.*?)\$\$/g, '<div class="my-4 p-4 bg-obs-l-bg-secondary text-obs-l-text-muted dark:bg-obs-d-bg-secondary dark:text-obs-d-text-muted rounded text-center">[LaTeX Equation: $1]</div>');
|
||||
|
||||
@@ -261,115 +123,251 @@ export class MarkdownService {
|
||||
this.tagColorCache.set(normalized, index);
|
||||
return `md-tag-color-${index}`;
|
||||
}
|
||||
private createMarkdownIt(env: MarkdownRenderEnv): MarkdownIt {
|
||||
const md = new MarkdownIt('commonmark', {
|
||||
html: true,
|
||||
linkify: false,
|
||||
typographer: false,
|
||||
breaks: false
|
||||
});
|
||||
|
||||
private convertTables(markdown: string): string {
|
||||
const lines = markdown.split('\n');
|
||||
const result: string[] = [];
|
||||
let index = 0;
|
||||
md.use(markdownItAttrs, {
|
||||
leftDelimiter: '{',
|
||||
rightDelimiter: '}',
|
||||
allowedAttributes: ['id', 'class']
|
||||
});
|
||||
|
||||
while (index < lines.length) {
|
||||
const currentLine = lines[index];
|
||||
if (this.isTableRow(currentLine) && this.isSeparatorRow(lines[index + 1] ?? '')) {
|
||||
const headerCells = this.splitTableRow(currentLine);
|
||||
const separatorLine = lines[index + 1];
|
||||
const rows: string[][] = [];
|
||||
index += 2;
|
||||
md.use(markdownItAnchor, {
|
||||
slugify: (str) => this.slugify(str),
|
||||
tabIndex: false
|
||||
});
|
||||
|
||||
while (index < lines.length && this.isTableRow(lines[index])) {
|
||||
rows.push(this.splitTableRow(lines[index]));
|
||||
index += 1;
|
||||
md.use(markdownItTaskLists, {
|
||||
enabled: false
|
||||
});
|
||||
|
||||
md.renderer.rules.fence = (tokens, idx, _options, renderEnv) => {
|
||||
const list = tokens as unknown as MarkdownItToken[];
|
||||
return this.renderFence(list[idx], renderEnv as MarkdownRenderEnv);
|
||||
};
|
||||
|
||||
md.renderer.rules.code_inline = (tokens, idx) => {
|
||||
const content = tokens[idx].content;
|
||||
return `<code class="inline-code">${this.escapeHtml(content)}</code>`;
|
||||
};
|
||||
|
||||
md.renderer.rules.heading_open = (tokens, idx, options, renderEnv, self) => {
|
||||
const list = tokens as unknown as MarkdownItToken[];
|
||||
const token = list[idx];
|
||||
const level = Number.parseInt(token.tag.replace('h', ''), 10);
|
||||
const headingClass = `md-heading md-heading-${level} text-obs-l-text-main dark:text-white font-bold mt-6 mb-3 pb-1 border-b border-obs-l-border dark:border-obs-d-border`;
|
||||
token.attrJoin('class', headingClass);
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
md.renderer.rules.blockquote_open = (tokens, idx, options, renderEnv, self) => {
|
||||
const list = tokens as unknown as MarkdownItToken[];
|
||||
const token = list[idx];
|
||||
token.attrJoin('class', 'border-l-4 border-gray-300 dark:border-gray-500 pl-4 py-2 my-4 italic text-gray-700 dark:text-gray-300');
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
md.renderer.rules.bullet_list_open = (tokens, idx, options, renderEnv, self) => {
|
||||
const list = tokens as unknown as MarkdownItToken[];
|
||||
const token = list[idx];
|
||||
token.attrJoin('class', 'mb-4 list-disc ml-6');
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
md.renderer.rules.ordered_list_open = (tokens, idx, options, renderEnv, self) => {
|
||||
const list = tokens as unknown as MarkdownItToken[];
|
||||
const token = list[idx];
|
||||
token.attrJoin('class', 'mb-4 list-decimal ml-6');
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
const defaultLinkOpen = md.renderer.rules.link_open ?? ((tokens, idx, options, renderEnv, self) => self.renderToken(tokens, idx, options));
|
||||
md.renderer.rules.link_open = (tokens, idx, options, renderEnv, self) => {
|
||||
const list = tokens as unknown as MarkdownItToken[];
|
||||
const token = list[idx];
|
||||
const href = token.attrGet('href') ?? '';
|
||||
if (/^(https?|ftp|file|mailto):\/\//i.test(href)) {
|
||||
token.attrSet('target', '_blank');
|
||||
token.attrSet('rel', 'noopener noreferrer');
|
||||
token.attrJoin('class', 'md-external-link');
|
||||
}
|
||||
return defaultLinkOpen(tokens, idx, options, renderEnv, self);
|
||||
};
|
||||
|
||||
md.renderer.rules.image = (tokens, idx) => {
|
||||
const list = tokens as unknown as MarkdownItToken[];
|
||||
const token = list[idx];
|
||||
const src = token.attrGet('src') ?? '';
|
||||
const title = token.attrGet('title');
|
||||
const alt = token.content ?? '';
|
||||
|
||||
if (!/^(https?|ftp|file):\/\//i.test(src)) {
|
||||
return `<img src="${this.escapeHtml(src)}" alt="${this.escapeHtml(alt)}"${title ? ` title="${this.escapeHtml(title)}"` : ''}>`;
|
||||
}
|
||||
|
||||
const safeUrl = this.escapeHtml(src);
|
||||
const safeAlt = this.escapeHtml(alt);
|
||||
const safeTitle = title ? this.escapeHtml(title) : '';
|
||||
const titleAttr = title ? ` title="${safeTitle}"` : '';
|
||||
const captionSource = title && title.trim() ? title : alt;
|
||||
const safeCaption = captionSource ? this.escapeHtml(captionSource) : '';
|
||||
const figcaption = safeCaption ? `<figcaption class="text-center text-sm text-obs-l-text-muted dark:text-obs-d-text-muted">${safeCaption}</figcaption>` : '';
|
||||
|
||||
return `<figure class="my-4 md-attachment-figure">
|
||||
<img src="${safeUrl}" alt="${safeAlt}"${titleAttr} loading="lazy" class="rounded-lg max-w-full h-auto mx-auto">
|
||||
${figcaption}
|
||||
</figure>`;
|
||||
};
|
||||
|
||||
md.renderer.rules.hr = () => '<hr class="my-6 border-obs-l-border dark:border-obs-d-border">';
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
private renderFence(token: MarkdownItToken, env: MarkdownRenderEnv): string {
|
||||
const rawLanguage = (token.info ?? '').trim();
|
||||
const normalizedLanguage = rawLanguage.toLowerCase();
|
||||
const isMermaid = normalizedLanguage === 'mermaid';
|
||||
const languageLabel = rawLanguage || 'plaintext';
|
||||
const languageDisplay = rawLanguage ? rawLanguage : 'Plain text';
|
||||
const encodedRawCode = encodeURIComponent(token.content);
|
||||
const safeLanguageDisplay = this.escapeHtml(languageDisplay);
|
||||
const codeId = `code-block-${env.codeBlockIndex++}`;
|
||||
|
||||
let bodyHtml: string;
|
||||
|
||||
if (isMermaid) {
|
||||
bodyHtml = `
|
||||
<div class="code-block__body mermaid-block__body">
|
||||
<div class="mermaid-diagram" data-mermaid-code="${encodedRawCode}"></div>
|
||||
<pre class="mermaid-source" hidden><code class="hljs" data-raw-code="${encodedRawCode}">${this.escapeHtml(token.content)}</code></pre>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
const highlightedCode = this.highlightCode(token.content, normalizedLanguage);
|
||||
bodyHtml = `<pre class="code-block__body"><code class="hljs" data-raw-code="${encodedRawCode}">${highlightedCode}</code></pre>`;
|
||||
}
|
||||
|
||||
const headerHtml = `
|
||||
<div class="code-block__header">
|
||||
<div class="code-block__controls" aria-hidden="true">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="code-block__actions">
|
||||
<button type="button" class="code-block__language-badge" data-language="${languageLabel.toLowerCase()}" data-code-id="${codeId}">${safeLanguageDisplay}</button>
|
||||
<span class="code-block__copy-feedback" hidden></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const wrapperClass = isMermaid ? 'code-block code-block--mermaid text-left' : 'code-block text-left';
|
||||
return `<div class="${wrapperClass}" style="max-width: 800px; width: 100%; margin: 0;" data-language="${languageLabel.toLowerCase()}" data-code-id="${codeId}">${headerHtml}${bodyHtml}</div>`;
|
||||
}
|
||||
|
||||
private highlightCode(code: string, language: string): string {
|
||||
if (language && hljs.getLanguage(language)) {
|
||||
return hljs.highlight(code, { language }).value;
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
|
||||
private transformCallouts(html: string): string {
|
||||
return html.replace(/<blockquote([\s\S]*?)>([\s\S]*?)<\/blockquote>/g, (match, _attrs, inner) => {
|
||||
const firstParagraphMatch = inner.match(/^\s*<p>([\s\S]*?)<\/p>([\s\S]*)$/);
|
||||
if (!firstParagraphMatch) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const markerMatch = firstParagraphMatch[1].match(/^\s*\[!(\w+)\]\s*([\s\S]*)$/);
|
||||
if (!markerMatch) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const type = markerMatch[1].toUpperCase();
|
||||
const calloutClass = this.getCalloutClass(type);
|
||||
const title = this.formatCalloutTitle(type);
|
||||
const remainingFirstParagraph = markerMatch[2].trim();
|
||||
const restContent = firstParagraphMatch[2].trim();
|
||||
|
||||
const bodySegments: string[] = [];
|
||||
if (remainingFirstParagraph) {
|
||||
bodySegments.push(`<p>${remainingFirstParagraph}</p>`);
|
||||
}
|
||||
if (restContent) {
|
||||
bodySegments.push(restContent);
|
||||
}
|
||||
|
||||
const bodyHtml = bodySegments.join('');
|
||||
|
||||
return `<div class="${calloutClass} text-left" style="max-width: 800px; width: 100%; margin: 0;" data-callout-type="${type.toLowerCase()}"><div class="callout__title">${title}</div><div class="callout__body">${bodyHtml}</div></div>`;
|
||||
});
|
||||
}
|
||||
|
||||
private transformTaskLists(html: string): string {
|
||||
const listRegex = /<ul class="([^"]*?task-list[^"]*?)">/g;
|
||||
html = html.replace(listRegex, (_match, classAttr) => {
|
||||
const classes = classAttr.split(/\s+/).filter(Boolean).filter(cls => !['task-list', 'list-disc', 'list-decimal', 'ml-6'].includes(cls));
|
||||
const classList = Array.from(new Set([...classes, 'md-task-list', 'mb-4']));
|
||||
return `<ul class="${classList.join(' ')}">`;
|
||||
});
|
||||
|
||||
const itemRegex = /<li class="task-list-item([^"]*)">([\s\S]*?)<\/li>/g;
|
||||
return html.replace(itemRegex, (_match, extraClasses, inner) => {
|
||||
const isChecked = /<input[^>]*\schecked[^>]*>/i.test(inner);
|
||||
const classes = ['md-task-item', 'text-obs-l-text-main', 'dark:text-obs-d-text-main'];
|
||||
if (isChecked) {
|
||||
classes.push('md-task-item--done');
|
||||
}
|
||||
|
||||
const trimmedInner = inner.trim();
|
||||
const inputMatch = trimmedInner.match(/^<input[^>]*>/i);
|
||||
if (!inputMatch) {
|
||||
return _match;
|
||||
}
|
||||
|
||||
const remaining = trimmedInner.slice(inputMatch[0].length).trim();
|
||||
let primaryContent = remaining;
|
||||
let trailingContent = '';
|
||||
|
||||
const paragraphMatch = remaining.match(/^<p>([\s\S]*?)<\/p>([\s\S]*)$/);
|
||||
if (paragraphMatch) {
|
||||
primaryContent = paragraphMatch[1].trim();
|
||||
trailingContent = paragraphMatch[2].trim();
|
||||
}
|
||||
|
||||
if (!paragraphMatch && remaining.includes('<')) {
|
||||
const firstTagIndex = remaining.indexOf('<');
|
||||
if (firstTagIndex > 0) {
|
||||
primaryContent = remaining.slice(0, firstTagIndex).trim();
|
||||
trailingContent = remaining.slice(firstTagIndex).trim();
|
||||
}
|
||||
|
||||
const columnCount = Math.max(
|
||||
headerCells.length,
|
||||
...rows.map(row => row.length)
|
||||
);
|
||||
const alignments = this.parseAlignments(separatorLine, columnCount);
|
||||
const headerHtml = Array.from({ length: columnCount }, (_, colIndex) => {
|
||||
const content = headerCells[colIndex] ?? '';
|
||||
return this.buildTableCell('th', content, alignments[colIndex]);
|
||||
}).join('');
|
||||
|
||||
const bodyHtml = rows.map(row => {
|
||||
const cells = Array.from({ length: columnCount }, (_, colIndex) => {
|
||||
const content = row[colIndex] ?? '';
|
||||
return this.buildTableCell('td', content, alignments[colIndex]);
|
||||
}).join('');
|
||||
return `<tr>${cells}</tr>`;
|
||||
}).join('');
|
||||
|
||||
const tableHtml = `<div class="markdown-table"><table><thead><tr>${headerHtml}</tr></thead>${rows.length ? `<tbody>${bodyHtml}</tbody>` : '<tbody></tbody>'}</table></div>`;
|
||||
result.push(tableHtml);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(currentLine);
|
||||
index += 1;
|
||||
}
|
||||
const checkboxState = isChecked ? 'checked' : '';
|
||||
const ariaChecked = isChecked ? 'true' : 'false';
|
||||
const checkbox = `<input type="checkbox" class="md-task-checkbox" ${checkboxState} disabled aria-checked="${ariaChecked}" tabindex="-1">`;
|
||||
const labelText = primaryContent || '';
|
||||
const label = `<label class="md-task">${checkbox}<span class="md-task-label-text">${labelText}</span></label>`;
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
const extra = extraClasses.split(/\s+/).filter(Boolean).filter(cls => cls !== 'task-list-item');
|
||||
const classList = Array.from(new Set([...classes, ...extra]));
|
||||
|
||||
private isTableRow(line: string): boolean {
|
||||
if (!line) {
|
||||
return false;
|
||||
}
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) {
|
||||
return false;
|
||||
}
|
||||
const segments = this.splitTableRow(trimmed);
|
||||
return segments.length > 0;
|
||||
}
|
||||
|
||||
private isSeparatorRow(line: string): boolean {
|
||||
if (!line) {
|
||||
return false;
|
||||
}
|
||||
const segments = this.splitTableRow(line);
|
||||
if (!segments.length) {
|
||||
return false;
|
||||
}
|
||||
return segments.every(segment => {
|
||||
const normalized = segment.replace(/\s+/g, '');
|
||||
return /^:?-{3,}:?$/.test(normalized);
|
||||
const trailing = trailingContent ? trailingContent : '';
|
||||
return `<li class="${classList.join(' ')}">${label}${trailing}</li>`;
|
||||
});
|
||||
}
|
||||
|
||||
private splitTableRow(line: string): string[] {
|
||||
let trimmed = line.trim();
|
||||
if (trimmed.startsWith('|')) {
|
||||
trimmed = trimmed.slice(1);
|
||||
}
|
||||
if (trimmed.endsWith('|')) {
|
||||
trimmed = trimmed.slice(0, -1);
|
||||
}
|
||||
return trimmed.split('|').map(cell => cell.trim());
|
||||
}
|
||||
|
||||
private parseAlignments(separatorLine: string, columnCount: number): TableAlignment[] {
|
||||
const segments = this.splitTableRow(separatorLine);
|
||||
return Array.from({ length: columnCount }, (_, index) => {
|
||||
const segment = (segments[index] ?? '').trim();
|
||||
const normalized = segment.replace(/\s+/g, '');
|
||||
if (normalized.startsWith(':') && normalized.endsWith(':')) {
|
||||
return 'center';
|
||||
}
|
||||
if (normalized.startsWith(':')) {
|
||||
return 'left';
|
||||
}
|
||||
if (normalized.endsWith(':')) {
|
||||
return 'right';
|
||||
}
|
||||
return null;
|
||||
private wrapTables(html: string): string {
|
||||
return html.replace(/<table([\s\S]*?)>([\s\S]*?)<\/table>/g, (_match, attrs, inner) => {
|
||||
return `<div class="markdown-table"><table${attrs}>${inner}</table></div>`;
|
||||
});
|
||||
}
|
||||
|
||||
private buildTableCell(tag: 'th' | 'td', content: string, alignment: TableAlignment): string {
|
||||
const alignStyle = alignment ? ` style="text-align:${alignment};"` : '';
|
||||
return `<${tag}${alignStyle}>${content}</${tag}>`;
|
||||
}
|
||||
|
||||
private escapeHtml(unsafe: string): string {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
|
||||
Reference in New Issue
Block a user