mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
Enhance StructureView to normalize folder paths and improve page assignment logic
This commit is contained in:
@@ -4,6 +4,7 @@ import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Page } from '../../models';
|
||||
import { StructureItem } from './StructureItem';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
|
||||
export interface IStructureViewProps {
|
||||
pages: Page[];
|
||||
@@ -19,7 +20,7 @@ interface FolderNode {
|
||||
export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
|
||||
pages
|
||||
}: React.PropsWithChildren<IStructureViewProps>) => {
|
||||
|
||||
|
||||
const folderTree = useMemo(() => {
|
||||
const root: FolderNode = {
|
||||
name: '',
|
||||
@@ -31,23 +32,63 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
|
||||
const folderMap = new Map<string, FolderNode>();
|
||||
folderMap.set('', root);
|
||||
|
||||
// First pass: create all folder nodes
|
||||
pages.forEach(page => {
|
||||
// Helper to compute the normalized folder path for a page.
|
||||
// It ensures the page's folder starts with the `fmFolder` segment and
|
||||
// preserves any subpaths after that segment (so subfolders are created).
|
||||
const computeNormalizedFolderPath = (page: Page): string => {
|
||||
if (!page.fmFolder) {
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
|
||||
const folderPath = page.fmFolder;
|
||||
// Normalize path separators and remove leading/trailing slashes
|
||||
const normalizedPath = folderPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
|
||||
const fmFolder = page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
|
||||
// If we have a file path, use its directory (exclude the filename) to compute
|
||||
// the relative path. This avoids treating filenames as folder segments.
|
||||
const filePath = page.fmFilePath ? parseWinPath(page.fmFilePath).replace(/^\/+|\/+$/g, '') : '';
|
||||
const fileDir = filePath && filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')).replace(/^\/+|\/+$/g, '') : '';
|
||||
|
||||
if (fileDir) {
|
||||
// If the content folder is known, and the file directory starts with it,
|
||||
// replace that root with the fmFolder (preserving subfolders after it).
|
||||
if (page.fmPageFolder?.path) {
|
||||
const contentFolderPath = parseWinPath(page.fmPageFolder.path).replace(/^\/+|\/+$/g, '');
|
||||
if (fileDir.startsWith(contentFolderPath)) {
|
||||
const rel = fileDir.substring(contentFolderPath.length).replace(/^\/+|\/+$/g, '');
|
||||
return rel ? `${fmFolder}/${rel}` : fmFolder;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise try to find fmFolder as a directory segment in the fileDir
|
||||
const segments = fileDir.split('/').filter(Boolean);
|
||||
const fmIndex = segments.indexOf(fmFolder);
|
||||
if (fmIndex >= 0) {
|
||||
return segments.slice(fmIndex).join('/');
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: just use the fmFolder name
|
||||
return fmFolder;
|
||||
};
|
||||
|
||||
// First pass: create all folder nodes (ensure nodes exist even if a page lacks fmFilePath)
|
||||
for (const page of pages) {
|
||||
if (!page.fmFolder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedPath = computeNormalizedFolderPath(page).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
if (!normalizedPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parts = normalizedPath.split('/').filter(part => part.length > 0);
|
||||
|
||||
|
||||
let currentPath = '';
|
||||
let currentNode = root;
|
||||
|
||||
parts.forEach(part => {
|
||||
|
||||
for (const part of parts) {
|
||||
const fullPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
|
||||
|
||||
if (!folderMap.has(fullPath)) {
|
||||
const newNode: FolderNode = {
|
||||
name: part,
|
||||
@@ -58,31 +99,31 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
|
||||
folderMap.set(fullPath, newNode);
|
||||
currentNode.children.push(newNode);
|
||||
}
|
||||
|
||||
|
||||
const nextNode = folderMap.get(fullPath);
|
||||
if (nextNode) {
|
||||
currentNode = nextNode;
|
||||
}
|
||||
currentPath = fullPath;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: assign pages to their folders
|
||||
pages.forEach(page => {
|
||||
// Second pass: assign pages to their exact folder node (including subfolders)
|
||||
for (const page of pages) {
|
||||
if (!page.fmFolder) {
|
||||
root.pages.push(page);
|
||||
} else {
|
||||
// Normalize the folder path for lookup
|
||||
const normalizedPath = page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
const folderNode = folderMap.get(normalizedPath);
|
||||
if (folderNode) {
|
||||
folderNode.pages.push(page);
|
||||
} else {
|
||||
// If folder not found, add to root as fallback
|
||||
root.pages.push(page);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
const normalizedPath = computeNormalizedFolderPath(page).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
const folderNode = normalizedPath ? folderMap.get(normalizedPath) : folderMap.get(page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''));
|
||||
if (folderNode) {
|
||||
folderNode.pages.push(page);
|
||||
} else {
|
||||
// If folder not found, add to root as fallback
|
||||
root.pages.push(page);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort folders and pages
|
||||
const sortNode = (node: FolderNode) => {
|
||||
@@ -90,15 +131,15 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
|
||||
node.pages.sort((a, b) => a.title.localeCompare(b.title));
|
||||
node.children.forEach(sortNode);
|
||||
};
|
||||
|
||||
|
||||
sortNode(root);
|
||||
|
||||
|
||||
return root;
|
||||
}, [pages]);
|
||||
|
||||
const renderFolderNode = (node: FolderNode, depth = 0): React.ReactNode => {
|
||||
const hasContent = node.pages.length > 0 || node.children.length > 0;
|
||||
|
||||
|
||||
if (!hasContent) {
|
||||
return null;
|
||||
}
|
||||
@@ -110,6 +151,9 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
|
||||
// For root node, render children and pages directly
|
||||
return (
|
||||
<div>
|
||||
{/* Root level folders */}
|
||||
{node.children.map(child => renderFolderNode(child, depth + 1))}
|
||||
|
||||
{/* Root level pages */}
|
||||
{node.pages.length > 0 && (
|
||||
<div className="mb-6">
|
||||
@@ -125,9 +169,6 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Root level folders */}
|
||||
{node.children.map(child => renderFolderNode(child, depth + 1))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -137,14 +178,13 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
|
||||
<Disclosure defaultOpen={depth <= 1}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
<Disclosure.Button
|
||||
className="flex items-center w-full text-left"
|
||||
style={{ paddingLeft: `${paddingLeft}px` }}
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={`w-4 h-4 mr-2 transform transition-transform ${
|
||||
open ? 'rotate-90' : ''
|
||||
}`}
|
||||
className={`w-4 h-4 mr-2 transform transition-transform ${open ? 'rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
<FolderIcon className="w-4 h-4 mr-2 text-[var(--vscode-symbolIcon-folderForeground)]" />
|
||||
<span className="font-medium text-[var(--vscode-editor-foreground)]">
|
||||
@@ -158,6 +198,9 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
|
||||
</Disclosure.Button>
|
||||
|
||||
<Disclosure.Panel className="mt-2">
|
||||
{/* Child folders */}
|
||||
{node.children.map(child => renderFolderNode(child, depth + 1))}
|
||||
|
||||
{/* Pages in this folder */}
|
||||
{node.pages.length > 0 && (
|
||||
<ul className="space-y-1 mb-3">
|
||||
@@ -168,9 +211,6 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* Child folders */}
|
||||
{node.children.map(child => renderFolderNode(child, depth + 1))}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { I18nConfig } from '../../models';
|
||||
import { ContentFolder, I18nConfig } from '../../models';
|
||||
|
||||
export interface Page {
|
||||
// Properties for caching
|
||||
@@ -20,15 +20,16 @@ export interface Page {
|
||||
fmCategories: string[];
|
||||
fmContentType: string;
|
||||
fmDateFormat: string | undefined;
|
||||
fmPageFolder: ContentFolder | undefined;
|
||||
|
||||
// i18n fields
|
||||
fmDefaultLocale?: boolean;
|
||||
fmLocale?: I18nConfig;
|
||||
fmTranslations?: {
|
||||
fmTranslations?: {
|
||||
[locale: string]: {
|
||||
locale: I18nConfig;
|
||||
path: string;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
title: string;
|
||||
|
||||
@@ -219,6 +219,7 @@ export class PagesParser {
|
||||
const isDefaultLanguage = await i18n.isDefaultLanguage(filePath);
|
||||
const locale = await i18n.getLocale(filePath);
|
||||
const translations = await i18n.getTranslations(filePath);
|
||||
const pageFolder = await Folders.getPageFolderByFilePath(filePath);
|
||||
|
||||
const page: Page = {
|
||||
...article.data,
|
||||
@@ -241,6 +242,7 @@ export class PagesParser {
|
||||
fmContentType: contentType.name || DEFAULT_CONTENT_TYPE_NAME,
|
||||
fmBody: article?.content || '',
|
||||
fmDateFormat: dateFormat,
|
||||
fmPageFolder: pageFolder,
|
||||
// i18n properties
|
||||
fmDefaultLocale: isDefaultLanguage,
|
||||
fmLocale: locale,
|
||||
|
||||
Reference in New Issue
Block a user