Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dev-test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ collections: # A list of collections the CMS should be able to edit
label_singular: 'Page'
folder: _pages
create: true
preview_path: 'pages/{{slug}}'
preview_path_preserve_slashes: true
nested: { depth: 100, subfolders: false }
fields:
- label: Title
Expand Down
7 changes: 7 additions & 0 deletions packages/decap-cms-core/src/lib/__tests__/urlHelper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ describe('sanitizeSlug', () => {
'test_test',
);
});

it('preserves slashes if when requested', () => {
const input = '/this-is-a/nested/page';

expect(sanitizeSlug(input, slugConfig, false)).toEqual('this-is-a-nested-page');
expect(sanitizeSlug(input, slugConfig, true)).toEqual('this-is-a/nested/page');
});
});

describe('sanitizeChar', () => {
Expand Down
18 changes: 14 additions & 4 deletions packages/decap-cms-core/src/lib/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,19 @@ export function prepareSlug(slug: string) {
);
}

export function getProcessSegment(slugConfig?: CmsSlug, ignoreValues?: string[]) {
export function getProcessSegment(
slugConfig?: CmsSlug,
ignoreValues?: string[],
preserveSlashes?: boolean,
) {
return (value: string) =>
ignoreValues && ignoreValues.includes(value)
? value
: flow([value => String(value), prepareSlug, partialRight(sanitizeSlug, slugConfig)])(value);
: flow([
value => String(value),
prepareSlug,
partialRight(sanitizeSlug, slugConfig, preserveSlashes),
])(value);
}

export function slugFormatter(
Expand Down Expand Up @@ -188,15 +196,17 @@ export function previewUrlFormatter(
fields = addFileTemplateFields(entry.get('path'), fields, collection.get('folder'));
const dateFieldName = getDateField() || selectInferredField(collection, 'date');
const date = parseDateFromEntry(entry as unknown as Map<string, unknown>, dateFieldName);
const preserveSlashes =
collection.has('nested') || !!collection.get('preview_path_preserve_slashes');

// Prepare and sanitize slug variables only, leave the rest of the
// `preview_path` template as is.
const processSegment = getProcessSegment(slugConfig, [fields.get('dirname')]);
const processSegment = getProcessSegment(slugConfig, [fields.get('dirname')], preserveSlashes);
let compiledPath;

try {
compiledPath = compileStringTemplate(pathTemplate, date, slug, fields, processSegment);
} catch (err) {
} catch (err: any) {
// Print an error and ignore `preview_path` if both:
// 1. Date is invalid (according to DayJs), and
// 2. A date expression (eg. `{{year}}`) is used in `preview_path`
Expand Down
36 changes: 27 additions & 9 deletions packages/decap-cms-core/src/lib/urlHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import isString from 'lodash/isString';
import escapeRegExp from 'lodash/escapeRegExp';
import flow from 'lodash/flow';
import partialRight from 'lodash/partialRight';
import identity from 'lodash/identity';

import type { CmsSlug } from '../types/redux';

Expand Down Expand Up @@ -51,7 +52,14 @@ function validIRIChar(char: string) {
return uriChars.test(char) || ucsChars.test(char);
}

export function getCharReplacer(encoding: string, replacement: string) {
export function getCharReplacer(
encoding: string,
options: {
replacement: NonNullable<CmsSlug['sanitize_replacement']>;
preserveSlashes?: boolean;
},
) {
const { replacement, preserveSlashes } = options;
let validChar: (char: string) => boolean;

if (encoding === 'unicode') {
Expand All @@ -67,14 +75,24 @@ export function getCharReplacer(encoding: string, replacement: string) {
throw new Error('The replacement character(s) (options.replacement) is itself unsafe.');
}

return (char: string) => (validChar(char) ? char : replacement);
return (char: string, i = 0, arr: string[] = [char]) => {
if (preserveSlashes && char === '/' && i !== 0 && i !== arr.length - 1) {
return char;
}

return validChar(char) ? char : replacement;
};
}
// `sanitizeURI` does not actually URI-encode the chars (that is the browser's and server's job), just removes the ones that are not allowed.
export function sanitizeURI(
str: string,
options?: { replacement: CmsSlug['sanitize_replacement']; encoding: CmsSlug['encoding'] },
options?: {
replacement: CmsSlug['sanitize_replacement'];
encoding: CmsSlug['encoding'];
preserveSlashes?: boolean;
},
) {
const { replacement = '', encoding = 'unicode' } = options || {};
const { replacement = '', encoding = 'unicode', preserveSlashes } = options || {};

if (!isString(str)) {
throw new Error('The input slug must be a string.');
Expand All @@ -85,15 +103,15 @@ export function sanitizeURI(

// `Array.from` must be used instead of `String.split` because
// `split` converts things like emojis into UTF-16 surrogate pairs.
return Array.from(str).map(getCharReplacer(encoding, replacement)).join('');
return Array.from(str).map(getCharReplacer(encoding, { replacement, preserveSlashes })).join('');
}

export function sanitizeChar(char: string, options?: CmsSlug) {
const { encoding = 'unicode', sanitize_replacement: replacement = '' } = options || {};
return getCharReplacer(encoding, replacement)(char);
return getCharReplacer(encoding, { replacement })(char);
}

export function sanitizeSlug(str: string, options?: CmsSlug) {
export function sanitizeSlug(str: string, options?: CmsSlug, preserveSlashes?: boolean) {
if (!isString(str)) {
throw new Error('The input slug must be a string.');
}
Expand All @@ -106,8 +124,8 @@ export function sanitizeSlug(str: string, options?: CmsSlug) {

const sanitizedSlug = flow([
...(stripDiacritics ? [diacritics.remove] : []),
partialRight(sanitizeURI, { replacement, encoding }),
partialRight(sanitizeFilename, { replacement }),
partialRight(sanitizeURI, { replacement, encoding, preserveSlashes }),
preserveSlashes ? identity : partialRight(sanitizeFilename, { replacement }),
])(str);

// Remove any doubled or leading/trailing replacement characters (that were added in the sanitizers).
Expand Down
1 change: 1 addition & 0 deletions packages/decap-cms-core/src/types/redux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,7 @@ type CollectionObject = {
public_folder?: string;
preview_path?: string;
preview_path_date_field?: string;
preview_path_preserve_slashes?: boolean;
summary?: string;
filter?: FilterRule;
type: 'file_based_collection' | 'folder_based_collection';
Expand Down