chore: update Angular cache with new TypeScript definitions

This commit is contained in:
Bruno Charest 2025-09-28 12:16:14 -04:00
parent f68440656e
commit 7f913ab33c
11 changed files with 349 additions and 27 deletions

File diff suppressed because one or more lines are too long

1
package-lock.json generated
View File

@ -17,6 +17,7 @@
"@angular/forms": "^20.3.1",
"@angular/platform-browser": "^20.3.0",
"angular-calendar": "^0.32.0",
"chokidar": "^4.0.3",
"date-fns": "^4.1.0",
"express": "^5.1.0",
"highlight.js": "^11.10.0",

View File

@ -222,6 +222,101 @@ if (!fs.existsSync(distDir)) {
// Servir les fichiers statiques de l'application Angular
app.use(express.static(distDir));
// Exposer les fichiers de la voûte pour un accès direct si nécessaire
app.use('/vault', express.static(vaultDir));
// Résolution des attachements: recherche le fichier en remontant les dossiers depuis la note, puis dans la voûte
app.get('/api/attachments/resolve', (req, res) => {
try {
const rawName = typeof req.query.name === 'string' ? req.query.name.trim() : '';
if (!rawName) {
return res.status(400).type('text/plain').send('Missing required query parameter: name');
}
const sanitize = (value) => value.replace(/\\/g, '/').replace(/^[/]+|[/]+$/g, '');
const name = sanitize(rawName);
const noteRelPath = typeof req.query.note === 'string' ? sanitize(req.query.note) : '';
const baseRaw = typeof req.query.base === 'string' ? req.query.base : '';
const baseRel = sanitize(baseRaw);
const candidateDirs = new Set();
const addCandidate = (dir, extra) => {
const dirSegments = sanitize(dir);
const extraSegments = sanitize(extra);
if (dirSegments && extraSegments) {
candidateDirs.add(`${dirSegments}/${extraSegments}`);
} else if (dirSegments) {
candidateDirs.add(dirSegments);
} else if (extraSegments) {
candidateDirs.add(extraSegments);
} else {
candidateDirs.add('');
}
};
// Dossiers parents de la note
if (noteRelPath) {
const segments = noteRelPath.split('/');
segments.pop(); // retirer le nom de fichier
while (segments.length >= 0) {
const currentDir = segments.join('/');
addCandidate(currentDir, baseRel);
addCandidate(currentDir, '');
if (!segments.length) break;
segments.pop();
}
}
// Si base est défini, tenter aussi directement depuis la racine
if (baseRel) {
addCandidate('', baseRel);
}
// Toujours ajouter la racine seule en dernier recours
addCandidate('', '');
for (const dir of candidateDirs) {
const absoluteDir = dir ? path.join(vaultDir, dir) : vaultDir;
const candidatePath = path.join(absoluteDir, name);
try {
if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isFile()) {
return res.sendFile(candidatePath);
}
} catch {
// Ignorer et poursuivre
}
}
// Recherche exhaustive en dernier recours (coût plus élevé)
const stack = [vaultDir];
const nameLower = name.toLowerCase();
while (stack.length) {
const currentDir = stack.pop();
let entries = [];
try {
entries = fs.readdirSync(currentDir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
} else if (entry.isFile() && entry.name.toLowerCase() === nameLower) {
return res.sendFile(fullPath);
}
}
}
return res.status(404).type('text/plain').send(`Attachement ${rawName} introuvable`);
} catch (error) {
console.error('Attachment resolve error:', error);
return res.status(500).type('text/plain').send('Attachment resolver internal error');
}
});
// API endpoint pour la santé
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });

View File

@ -95,7 +95,7 @@ export class AppComponent implements OnDestroy {
const note = this.selectedNote();
if (!note) return '';
const allNotes = this.vaultService.allNotes();
return this.markdownService.render(note.content, allNotes);
return this.markdownService.render(note.content, allNotes, note);
});
selectedNoteBreadcrumb = computed<string[]>(() => {

View File

@ -142,6 +142,8 @@ export class NoteViewerComponent implements OnDestroy {
private mermaidLib: MermaidLib | null = null;
private readonly dateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' });
private readonly metadataKeysToExclude = new Set(['tags', 'tag', 'keywords']);
private attachmentErrorCleanup: (() => void) | null = null;
private attachmentHandlersScheduled = false;
readonly metadataExpanded = signal(false);
readonly maxMetadataPreviewItems = 3;
@ -205,6 +207,7 @@ export class NoteViewerComponent implements OnDestroy {
effect(() => {
this.noteHtmlContent();
this.scheduleMermaidRender();
this.scheduleAttachmentHandlers();
});
afterNextRender(() => {
@ -247,6 +250,7 @@ export class NoteViewerComponent implements OnDestroy {
this.setupMermaidObservation();
this.scheduleMermaidRender();
this.scheduleAttachmentHandlers();
});
}
@ -255,6 +259,8 @@ export class NoteViewerComponent implements OnDestroy {
this.mermaidObserver = null;
this.mermaidLib = null;
this.mermaidLoader = null;
this.attachmentErrorCleanup?.();
this.attachmentErrorCleanup = null;
}
getFrontmatterKeys(frontmatter: { [key: string]: any }): string[] {
@ -312,6 +318,69 @@ export class NoteViewerComponent implements OnDestroy {
});
}
private scheduleAttachmentHandlers(): void {
if (this.attachmentHandlersScheduled) {
return;
}
this.attachmentHandlersScheduled = true;
queueMicrotask(() => {
this.attachmentHandlersScheduled = false;
this.attachAttachmentErrorHandlers();
});
}
private attachAttachmentErrorHandlers(): void {
this.attachmentErrorCleanup?.();
this.attachmentErrorCleanup = null;
const host = this.elementRef.nativeElement as HTMLElement;
const images = Array.from(host.querySelectorAll<HTMLImageElement>('img.md-attachment-image'));
if (!images.length) {
return;
}
const cleanupCallbacks: Array<() => void> = [];
const noteId = this.note()?.id ?? 'unknown-note';
for (const image of images) {
const handleError = () => {
image.removeEventListener('error', handleError);
const attachmentName = image.dataset.attachmentName?.trim() || image.alt || 'attachment';
const fallbackMarkup = image.dataset.errorMessage || `<div class=\"missing-attachment text-center text-sm text-red-500 dark:text-red-400\">‼Attachement ${attachmentName} introuvable</div>`;
console.warn('[ObsiViewer] Attachment missing', {
noteId,
attachmentName,
src: image.currentSrc || image.src,
});
try {
image.outerHTML = fallbackMarkup;
} catch {
const wrapper = document.createElement('div');
wrapper.innerHTML = fallbackMarkup;
const parent = image.parentElement;
const nodes = Array.from(wrapper.childNodes);
if (parent) {
nodes.forEach(node => parent.insertBefore(node, image));
image.remove();
}
}
};
image.addEventListener('error', handleError, { once: true });
cleanupCallbacks.push(() => image.removeEventListener('error', handleError));
}
this.attachmentErrorCleanup = () => {
cleanupCallbacks.forEach(fn => fn());
this.attachmentErrorCleanup = null;
};
}
private ensureMermaid(): Promise<MermaidLib> {
if (this.mermaidLib) {
return Promise.resolve(this.mermaidLib);

View File

@ -12,7 +12,7 @@ export class MarkdownService {
private readonly tagPaletteSize = 12;
private tagColorCache = new Map<string, number>();
render(markdown: string, allNotes: Note[]): string {
render(markdown: string, allNotes: Note[], currentNote?: Note): string {
let html = markdown;
// Process code blocks first to prevent inner content from being parsed
@ -101,11 +101,31 @@ export class MarkdownService {
});
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;
@ -116,29 +136,62 @@ export class MarkdownService {
if (inlineCode.includes('\n')) {
return match;
}
return `<code class="inline-code">${this.escapeHtml(inlineCode)}</code>`;
});
// Internal Links [[link|alias]] or [[link]]
html = html.replace(/\[\[([^|\]\n]+)(?:\|([^\]\n]+))?\]\]/g, (match, link, alias) => {
const targetText = alias || link;
const targetId = link.toLowerCase().replace(/\s+/g, '-');
const noteExists = allNotes.some(n => n.id === targetId || n.title === link);
const classes = noteExists ? 'text-obs-l-accent dark:text-obs-d-accent hover:underline cursor-pointer' : 'text-red-500 dark:text-red-400 cursor-not-allowed';
return `<a data-note-id="${targetId}" data-note-title="${link}" class="${classes}">${targetText}</a>`;
const safeContent = this.escapeHtml(inlineCode.trim());
return `<code class="inline-code">${safeContent}</code>`;
});
// External Links [text](url)
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline">$1</a>');
// 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>`;
});
// Embedded Files ![[file.png]]
html = html.replace(/!\[\[(.*?)\]\]/g, (match, filename) => {
return `<figure class="my-4"><img src="https://picsum.photos/600/400?random=${Math.random()}" alt="${filename}" class="rounded-lg max-w-full h-auto mx-auto"><figcaption class="text-center text-sm text-obs-l-text-muted dark:text-obs-d-text-muted mt-2">${filename}</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>`;
});
// Embedded Files ![[file.png]] using Attachements-Path from HOME.md (traité avant les liens internes)
const homeNote = allNotes.find(n =>
(n.fileName?.toLowerCase?.() === 'home.md') ||
(n.originalPath?.toLowerCase?.() === 'home') ||
(n.originalPath?.toLowerCase?.().endsWith('/home'))
);
const attachmentsBase = (() => {
if (!homeNote) return '';
const fm = homeNote.frontmatter || {};
const key = Object.keys(fm).find(k => k?.toLowerCase?.() === 'attachements-path' || k?.toLowerCase?.() === 'attachments-path');
const raw = key ? `${fm[key] ?? ''}` : '';
return raw;
})();
html = html.replace(/!\[\[(.*?)\]\]/g, (match, rawName) => {
const filename = `${rawName}`.trim();
const safeAlt = this.escapeHtml(filename);
const notePath = (currentNote?.filePath || currentNote?.originalPath || '').replace(/\\/g, '/');
const src = `/api/attachments/resolve?name=${encodeURIComponent(filename)}&note=${encodeURIComponent(notePath)}&base=${encodeURIComponent(attachmentsBase)}`;
return `<figure class="my-4 md-attachment-figure">
<img src="${src}" alt="${safeAlt}" loading="lazy" class="rounded-lg max-w-full h-auto mx-auto md-attachment-image" data-attachment-name="${safeAlt}" data-error-message="&lt;div class=&#39;missing-attachment text-center text-sm text-red-500 dark:text-red-400&#39;&gt;Attachement ${this.escapeHtml(filename).replace(/'/g, '&#39;')} introuvable&lt;/div&gt;">
<figcaption class="text-center text-sm text-obs-l-text-muted dark:text-obs-d-text-muted">${safeAlt}</figcaption>
</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>');

View File

@ -1,3 +1,87 @@
.md-attachment-figure {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.md-attachment-figure img {
margin-bottom: 0;
}
.md-task-list {
list-style: none;
margin: 0 0 1rem 0;
padding: 0;
}
.md-task-item {
list-style: none;
margin-bottom: 0.5rem;
}
.md-task {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.95rem;
}
.md-task-checkbox {
appearance: none;
width: 1.1rem;
height: 1.1rem;
border-radius: 0.35rem;
border: 2px solid var(--task-checkbox-border, #64748b);
background-color: transparent;
position: relative;
}
.md-task-item--done .md-task-checkbox {
border-color: var(--task-checkbox-checked-border, #22c55e);
background-color: var(--task-checkbox-checked-bg, #22c55e);
}
.md-task-item--done .md-task-label-text {
text-decoration: line-through;
color: var(--task-checkbox-done-text, #94a3b8);
}
.md-task-checkbox::after {
content: '';
position: absolute;
top: 0.05rem;
left: 0.25rem;
width: 0.35rem;
height: 0.65rem;
border-right: 2px solid white;
border-bottom: 2px solid white;
transform: rotate(45deg);
opacity: 0;
}
.md-task-item--done .md-task-checkbox::after {
opacity: 1;
}
.md-external-link {
color: var(--external-link, #6366f1);
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 3px;
transition: color 0.2s ease, text-decoration-color 0.2s ease;
}
.md-external-link:hover,
.md-external-link:focus-visible {
color: var(--external-link-hover, #4338ca);
text-decoration-color: currentColor;
}
.md-external-link:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@ -9,6 +93,24 @@
-webkit-overflow-scrolling: touch;
}
&:root {
--external-link: #6366f1;
--external-link-hover: #4338ca;
--task-checkbox-border: #94a3b8;
--task-checkbox-checked-border: #22c55e;
--task-checkbox-checked-bg: #22c55e;
--task-checkbox-done-text: #94a3b8;
}
.dark {
--external-link: #22d3ee;
--external-link-hover: #0ea5e9;
--task-checkbox-border: #64748b;
--task-checkbox-checked-border: #22c55e;
--task-checkbox-checked-bg: #15803d;
--task-checkbox-done-text: #64748b;
}
@media (min-width: 1024px) {
body {
height: 100vh;

View File

@ -2,11 +2,9 @@
Titre: Page d'accueil
NomDeVoute: IT
Description: Page d'accueil de la voute IT
tags: [home, accueil]
tags: [home, accueil, configuration]
attachements-path: attachements/
---
## TEST
bonjour
Page principal - IT
### allo
bonjour
alloooo
![[Voute_IT.png]]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

View File

@ -71,12 +71,16 @@ Citation en ligne : « > Ceci est une citation »
## Images
![[Voute_IT.png]]
![[Fichier_not_found.png]]
![[document_pdf.pdf]]
## Liens et images
[Lien vers le site officiel d&#39;Obsidian](https://obsidian.md)
[Lien vers le site officiel d'Obsidian](https://obsidian.md)
![Image de démonstration](https://via.placeholder.com/400x200 "Image de test")
![Image de démonstration](https://static0.howtogeekimages.com/wordpress/wp-content/uploads/2019/12/markdown-logo-on-a-blue-background.png?q=50&fit=crop&w=1200&h=675&dpr=1.5 "Image de test")
![Image de démonstration](https://static0.howtogeekimages.com/wordpress/wp-content/uploads/2019/12/markdown-logo-on-a-blue-background.png?q=50&fit=crop&w=1200&h=675&dpr=1.5)
## Tableaux