chore: update Angular cache with new TypeScript definitions
This commit is contained in:
parent
f68440656e
commit
7f913ab33c
2
.angular/cache/20.3.2/app/.tsbuildinfo
vendored
2
.angular/cache/20.3.2/app/.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
1
package-lock.json
generated
1
package-lock.json
generated
@ -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",
|
||||
|
@ -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() });
|
||||
|
@ -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[]>(() => {
|
||||
|
@ -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);
|
||||
|
@ -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>`;
|
||||
const safeContent = this.escapeHtml(inlineCode.trim());
|
||||
return `<code class="inline-code">${safeContent}</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>`;
|
||||
// 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)
|
||||
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 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]]
|
||||
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>`;
|
||||
// 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)}¬e=${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="<div class='missing-attachment text-center text-sm text-red-500 dark:text-red-400'>Attachement ${this.escapeHtml(filename).replace(/'/g, ''')} introuvable</div>">
|
||||
<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>');
|
||||
|
||||
|
102
src/styles.css
102
src/styles.css
@ -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;
|
||||
|
@ -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]]
|
||||
|
BIN
vault/attachements/Voute_IT.png
Normal file
BIN
vault/attachements/Voute_IT.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
BIN
vault/attachements/document_pdf.pdf
Normal file
BIN
vault/attachements/document_pdf.pdf
Normal file
Binary file not shown.
@ -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'Obsidian](https://obsidian.md)
|
||||
[Lien vers le site officiel d'Obsidian](https://obsidian.md)
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
## Tableaux
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user