Skip to content

Commit

Permalink
Tighten entry matching logic with i18n
Browse files Browse the repository at this point in the history
Fix #242
  • Loading branch information
kyoshino committed Nov 1, 2024
1 parent 8530f21 commit 276aa0f
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 45 deletions.
23 changes: 12 additions & 11 deletions src/lib/services/contents/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { getPathInfo } from '@sveltia/utils/file';
import { stripSlashes } from '@sveltia/utils/string';
import { get, writable } from 'svelte/store';
import { allAssetFolders, getMediaFieldURL } from '$lib/services/assets';
import { siteConfig } from '$lib/services/config';
import { getFieldConfig, getPropertyValue } from '$lib/services/contents/entry';
import { getI18nConfig } from '$lib/services/contents/i18n';
import { getFileExtension } from '$lib/services/contents/parser';
import { getEntryPathRegEx } from '$lib/services/contents/parser';

/**
* Regular expression to match `![alt](src "title")`.
Expand Down Expand Up @@ -119,21 +118,23 @@ export const getCollection = (name) => {
* @returns {CollectionEntryFolder[]} Entry folders. Sometimes it’s hard to find the right folder
* because multiple collections can have the same folder or partially overlapping folder paths, but
* the first one is most likely what you need.
* @todo Make the logic more diligent, taking i18n config into account.
*/
export const getEntryFoldersByPath = (path) => {
const { extension } = getPathInfo(path);

return get(allEntryFolders)
.filter(({ filePathMap, folderPath, parserConfig }) => {
export const getEntryFoldersByPath = (path) =>
get(allEntryFolders)
.filter(({ collectionName, filePathMap }) => {
if (filePathMap) {
return Object.values(filePathMap ?? {}).includes(path);
return Object.values(filePathMap).includes(path);
}

return path.startsWith(`${folderPath}/`) && getFileExtension(parserConfig) === extension;
const collection = getCollection(collectionName);

if (!collection) {
return false;
}

return !!path.match(getEntryPathRegEx(collection));
})
.sort((a, b) => b.folderPath?.localeCompare(a.folderPath ?? '') ?? 0);
};

/**
* Get a list of collections the given entry belongs to. One entry can theoretically appear in
Expand Down
80 changes: 46 additions & 34 deletions src/lib/services/contents/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,43 @@ const getSlug = (collectionName, filePath, content) => {
);
};

/**
* Get a regular expression that matches the entry paths of the given folder collection, taking i18n
* into account.
* @param {Collection} collection - Collection.
* @returns {RegExp} Regular expression.
*/
export const getEntryPathRegEx = (collection) => {
const {
extension,
format,
folder,
path,
_i18n: { i18nEnabled, locales, structure },
} = collection;

const folderPath = stripSlashes(/** @type {string} */ (folder));
const i18nMultiFiles = i18nEnabled && structure === 'multiple_files';
const i18nMultiFolders = i18nEnabled && structure === 'multiple_folders';
const ext = getFileExtension({ format, extension });
/**
* The path pattern in the middle, which should match the filename (without extension),
* possibly with the parent directory. If the collection’s `path` is configured, use it to
* generate a pattern, so that unrelated files are excluded.
* @see https://decapcms.org/docs/collection-folder/#folder-collections-path
*/
const filePathMatcher = path ? path.replace(/\//g, '\\/').replace(/{{.+?}}/g, '[^\\/]+') : '.+';
const localeMatcher = locales.join('|');

return new RegExp(
`^${escapeRegExp(stripSlashes(folderPath))}\\/` +
`${i18nMultiFolders ? `(?<locale>${localeMatcher})\\/` : ''}` +
`(?<filePath>${filePathMatcher})` +
`${i18nMultiFiles ? `\\.(?<locale>${localeMatcher})` : ''}` +
`\\.${ext}$`,
);
};

/**
* Parse the given entry files to create a complete, serialized entry list.
* @param {BaseEntryListItem[]} entryFiles - Entry file list.
Expand Down Expand Up @@ -353,12 +390,7 @@ export const parseEntryFiles = (entryFiles) => {
path,
sha,
meta = {},
folder: {
folderPath: configFolderPath = '',
collectionName,
fileName: collectionFileName,
filePathMap,
},
folder: { collectionName, fileName, filePathMap },
} = file;

const collection = getCollection(collectionName);
Expand All @@ -367,9 +399,7 @@ export const parseEntryFiles = (entryFiles) => {
return;
}

const collectionFile = collectionFileName
? collection._fileMap?.[collectionFileName]
: undefined;
const collectionFile = fileName ? collection._fileMap?.[fileName] : undefined;

const {
fields = [],
Expand Down Expand Up @@ -414,15 +444,15 @@ export const parseEntryFiles = (entryFiles) => {
const extension = getFileExtension({
format: collection.format,
extension: collection.extension,
file: collectionFileName,
file: fileName,
});

// Skip Hugo’s special index page that shouldn’t appear in a folder collection, unless the
// collection’s `path` ends with `_index` and the extension is `md`.
if (
getPathInfo(path).basename === '_index.md' &&
!(collection.path?.split('/').pop() === '_index' && extension === 'md') &&
!collectionFileName
!fileName
) {
return;
}
Expand All @@ -432,33 +462,15 @@ export const parseEntryFiles = (entryFiles) => {
/** @type {LocaleCode | undefined} */
let locale = undefined;

if (collectionFileName) {
if (fileName) {
if (i18nMultiFiles || i18nMultiFolders) {
[locale, filePath] =
Object.entries(filePathMap ?? {}).find(([, locPath]) => locPath === path) ?? [];
} else {
filePath = path;
}
} else {
/**
* The path pattern in the middle, which should match the filename (without extension),
* possibly with the parent directory. If the collection’s `path` is configured, use it to
* generate a pattern, so that unrelated files are excluded.
* @see https://decapcms.org/docs/collection-folder/#folder-collections-path
*/
const filePathMatcher = collection.path
? collection.path.replace(/\//g, '\\/').replace(/{{.+?}}/g, '[^\\/]+')
: '.+';

const regex = new RegExp(
`^${escapeRegExp(stripSlashes(configFolderPath))}\\/` +
`${i18nMultiFolders ? `(?<locale>${locales.join('|')})\\/` : ''}` +
`(?<filePath>${filePathMatcher})` +
`${i18nMultiFiles ? `\\.(?<locale>${locales.join('|')})` : ''}` +
`\\.${extension}$`,
);

({ filePath, locale } = path.match(regex)?.groups ?? {});
({ filePath, locale } = path.match(getEntryPathRegEx(collection))?.groups ?? {});
}

if (!filePath) {
Expand All @@ -469,15 +481,15 @@ export const parseEntryFiles = (entryFiles) => {
const entry = { id: '', slug: '', sha, locales: {}, ...meta };

if (!i18nEnabled) {
const slug = collectionFileName || getSlug(collectionName, filePath, parsedFile);
const slug = fileName || getSlug(collectionName, filePath, parsedFile);

entry.slug = slug;
entry.locales._default = { slug, path, sha, content: flatten(parsedFile) };
}

if (i18nSingleFile) {
const content = parsedFile[defaultLocale] ?? Object.values(parsedFile)[0];
const slug = collectionFileName || getSlug(collectionName, filePath, content);
const slug = fileName || getSlug(collectionName, filePath, content);

entry.slug = slug;
entry.locales = Object.fromEntries(
Expand All @@ -492,7 +504,7 @@ export const parseEntryFiles = (entryFiles) => {
return;
}

const slug = collectionFileName || getSlug(collectionName, filePath, parsedFile);
const slug = fileName || getSlug(collectionName, filePath, parsedFile);
const localizedEntry = { slug, path, sha, content: flatten(parsedFile) };
// Support a canonical slug to link localized files
const canonicalSlug = parsedFile[canonicalSlugKey];
Expand Down

0 comments on commit 276aa0f

Please sign in to comment.