chore: update Angular and TypeScript type definitions in cache

This commit is contained in:
2025-09-28 13:29:41 -04:00
parent 7f913ab33c
commit 963d0cc323
81 changed files with 7444 additions and 663 deletions
+254 -256
View File
@@ -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 ![alt](url "title")
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, "&amp;")