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
16 changes: 13 additions & 3 deletions packages/security/lib/string.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
const slugify = require('@tryghost/string').slugify;

// @TODO: the safe() function can possibly be removed from here if Ghost uses slugify() directly instead

/**
* @param {string} string
* @param {{importing?: boolean}} [options]
* @param {string} string - the string we want to slugify
* @param {object} [options] - filter options
* @param {bool} [options.importing] - don't perform optional cleanup, e.g. removing extra dashes
* @param {bool} [options.unicodeSlugs] - don't perform optional transliteration, e.g. keep smörgåsbord as it is instead of turning it into smorgasbord
* @param {string} [options.slugSeparator] - separator to be used for the slugs, can be ` `, `_` or `-`, defaults to `-`
* @returns {string} - a slugified string
*/
module.exports.safe = function safe(string, options = {}) {
return slugify(string, { requiredChangesOnly: options.importing === true });
return slugify(string, {
requiredChangesOnly: options.importing === true,
unicodeSlugs: options.unicodeSlugs === true,
slugSeparator: options.slugSeparator
Comment on lines +14 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node <<'NODE'
const fs = require('fs');
const path = require('path');

const entry = require.resolve('`@tryghost/string`');
const packageJson = JSON.parse(
    fs.readFileSync(path.join(path.dirname(entry), '..', 'package.json'), 'utf8')
);
const {slugify} = require('`@tryghost/string`');

const unicode = slugify('Ett smörgåsbord från Sydkorea: 스뫼르고스보르드', {
    unicodeSlugs: true
});
const separated = slugify('Herr./Klaus-Jürgen_44', {
    slugSeparator: ' '
});

console.log({version: packageJson.version, unicode, separated});

if (unicode !== 'ett-smörgåsbord-från-sydkorea-스뫼르고스보르드') process.exit(1);
if (separated !== 'herr klaus-jurgen_44') process.exit(1);
NODE

Repository: TryGhost/framework

Length of output: 957


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dependency declarations =="
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$' | while read -r f; do
  echo "--- $f"
  rg -n '"`@tryghost/string`"|`@tryghost/string`|slugSeparator|unicodeSlugs' "$f" -C 2 || true
done

echo
echo "== relevant source =="
fd -a 'package.json$' . | while read -r f; do
  case "$f" in
    */node_modules/*) continue ;;
  esac
  if rg -q '`@tryghost/string`' "$f"; then
    echo "--- $f"
    sed -n '/@tryghost\/string/,+2p' "$f"
  fi
done

echo
echo "== read package file =="
if [ -f packages/security/lib/string.js ]; then
  cat -n packages/security/lib/string.js | sed -n '1,80p'
fi

echo
echo "== lockfile snippets =="
if [ -f pnpm-lock.yaml ]; then
  rg -n 'tryghost/string|/(`@tryghost/string`):|slugSeparator|unicodeSlugs' pnpm-lock.yaml -C 3 || true
fi

Repository: TryGhost/framework

Length of output: 4673


🌐 Web query:

@tryghost/string 0.3.5 npm source slugify unicodeSlugs slugSeparator

💡 Result:

The package @tryghost/string version 0.3.5 is a utility library maintained by the Ghost Foundation, primarily used within the Ghost publishing platform [1][2][3]. Regarding your specific search terms: 1. slugify: The package provides slugification functionality used to convert strings into URL-friendly slugs [4][5]. In modern versions of Ghost, the slugification logic is designed to handle character encoding specifically to ensure compatibility across browsers [4][6]. 2. unicodeSlugs: There is no native option or parameter named unicodeSlugs in the @tryghost/string library. Ghost's default slugification process often strips non-ASCII characters or uses encoding methods to produce predictable, ASCII-compatible slugs [4][7][6]. Users seeking to support non-Latin (Unicode) characters in slugs often find it necessary to manually modify the underlying slugify logic in their Ghost installation, as there is no built-in configuration option for this [5][7][6]. 3. slugSeparator: Similar to unicodeSlugs, there is no explicit parameter named slugSeparator exposed in the public interface of this package to configure the separator character (which defaults to a hyphen) [4]. Because @tryghost/string is a private or internal-facing utility for the Ghost project, it lacks public, comprehensive API documentation [8][9]. The implementation of slugification is typically found in the core codebase of Ghost, where it is used to process titles and other strings into slugs [4][5][7]. If you require specific customization, you would need to inspect the source code within the Ghost repository or the specific version of the package installed in your node_modules directory [4][7].

Citations:


🌐 Web query:

npmx.dev @tryghost/string v0.3.5 slugify.js unicodeSlugs slugSeparator

💡 Result:

The @tryghost/string package, including version 0.3.5 [1][2], is a utility library maintained by the Ghost Foundation, commonly used within the Ghost ecosystem to handle string operations such as URL slug generation [3][4][5]. Regarding the specific terms mentioned: slugify.js: This is a core component within the @tryghost/string package (and historically within other internal Ghost packages like @tryghost/kg-utils) that provides the logic for converting strings into URL-safe slugs [6][3][7]. The implementation typically involves replacing reserved URL characters, handling whitespace, and normalizing the string to lowercase [6]. unicodeSlugs and slugSeparator: These terms are not explicit configuration options or variables found within the standard slugify.js implementation of @tryghost/string. The library’s slugification logic is generally prescriptive rather than highly configurable via these specific names [6]. While different versions of Ghost have evolved their slugification strategies—moving from aggressive character replacement (using dashes as separators) to URL encoding in newer versions—they do not expose a public API using the specific identifiers "unicodeSlugs" or "slugSeparator" [8][9]. If you are looking for advanced configuration for slug generation (such as custom separators or specific Unicode handling), libraries like @sindresorhus/slugify are often used in the broader JavaScript ecosystem as they provide explicit options for features like separators and Unicode transliteration [10].

Citations:


🌐 Web query:

"app.unpkg.com" "@tryghost/string@0.3.5" "unicodeSlugs" "slugSeparator"

💡 Result:

The package @tryghost/string version 0.3.5 is a utility library maintained by the Ghost project, primarily used for string processing tasks such as generating URL slugs [1]. Technical analysis of the library and its usage within the Ghost ecosystem indicates the following regarding the terms mentioned: 1. unicodeSlugs: There is no native configuration option named unicodeSlugs in the @tryghost/string library [1][2]. Ghost's default slugification behavior historically strips non-ASCII characters or encodes them [3][2]. For example, in newer versions (4.x+), the slugify function uses encodeURIComponent to handle non-Latin characters, which results in percent-encoded strings rather than retaining the original Unicode characters in the URL slug [3][2]. Users requiring full Unicode support in slugs often have to implement custom modifications to the library's slugify logic or validator expressions to override these constraints [4]. 2. slugSeparator: Similarly, the library does not expose a public configuration option named slugSeparator to change the default character used to separate words in slugs [1]. The internal implementation of the slugify function defaults to using a hyphen (-) as the separator [3][2]. If you are encountering these terms, they are likely references to custom implementations, intended features in a different context, or modifications made by users attempting to bypass the library's default ASCII-focused or hyphen-delimited behavior [4]. For standard usage, the library follows the patterns defined in the Ghost codebase, which utilize a hardcoded hyphen separator and sanitize inputs to maintain URL compatibility [3][2].

Citations:


🌐 Web query:

@tryghost/string package 0.3.5 npm source "slugSeparator" "unicodeSlugs"

💡 Result:

The @tryghost/string package, specifically version 0.3.5, is an internal utility package maintained by the Ghost organization for string manipulation, including slug generation [1][2]. Regarding your specific queries: 1. slugSeparator: The @tryghost/string package uses a hardcoded hyphen (-) as the slug separator in modern versions (Ghost 4.x and later) [3][4]. The logic within the package is designed to replace whitespace with hyphens and strip or URL-encode other characters [3][4]. There is no configurable slugSeparator option exposed in the public API of this package [3]. 2. unicodeSlugs: The package historically prioritizes ASCII-friendly URLs [5][6]. While Ghost's slugification logic has evolved, it typically performs operations like removing non-word characters or using encodeURIComponent to handle non-ASCII input [3][4]. It does not natively support "unicode slugs" in the sense of keeping raw UTF-8 characters in the URL path; instead, it tends to strip non-ASCII characters or encode them [3][4]. Users seeking to support non-Latin languages in URLs have historically had to patch the package or modify core Ghost files to bypass these restrictions [6]. Because this is an internal package used by the Ghost platform, documentation is minimal and tailored to the platform's internal requirements [2][7]. It is not intended for general public use as a standalone library [7].

Citations:


Make the supported slugify options match the workspace dependency.

packages/security/lib/string.js passes unicodeSlugs and slugSeparator, but the lockfile resolves @tryghost/string to 0.3.5, and that build exposes unidecode backing rather than those custom options. Either upgrade/patch the workspace dependency to support these options or keep passing only the options that the resolved @tryghost/string@0,3.5 implementation honors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/security/lib/string.js` around lines 14 - 17, Align the options
passed by the slugify call in the string helper with the resolved
`@tryghost/string`@0.3.5 API: either update or patch that dependency to support
unicodeSlugs and slugSeparator, or remove those unsupported options and retain
only the options it honors, including requiredChangesOnly.

});
};
29 changes: 23 additions & 6 deletions packages/security/test/string.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('Lib: Security - String', function () {
assert.equal(result, '');
});

it('should remove non ascii characters', function () {
it('should remove non-letter characters', function () {
const result = security.string.safe('howtowin✓', options);
assert.equal(result, 'howtowin');
});
Expand All @@ -37,9 +37,9 @@ describe('Lib: Security - String', function () {
// note: This is missing the soft-hyphen char that isn't much-liked by linters/browsers/etc,
// it passed the test before it was removed
const result = security.string.safe(
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿',
'!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~¡¢£¤¥¦§¨©ª«¬®¯°±²_³´µ¶·¸¹º»¼½¾¿',
);
assert.equal(result, '_-c-y-ss-c-a-r-deg-23up-1o-1-41-23-4');
assert.equal(result, 'a-2_3u-1o-1-41-23-4');
});

it('should replace all of the foreign chars in ascii', function () {
Expand All @@ -48,7 +48,7 @@ describe('Lib: Security - String', function () {
);
assert.equal(
result,
'aaaaaaaeceeeeiiiidnoooooxouuuuythssaaaaaaaeceeeeiiiidnooooo-ouuuuythy',
'aaaaaaae-ceeeeiiiidnooooo-ouuuuythssaaaaaaaeceeeeiiiidnooooo-ouuuuythy',
);
});

Expand Down Expand Up @@ -83,16 +83,33 @@ describe('Lib: Security - String', function () {
});

it('should properly handle unicode punctuation conversion', function () {
// note: the previous unidecode transformation handled this differently than anyascii, so this is
// a compromise that's "good enough" and gives the most optimal results for most languages
// result using unidecode was: nijian-wei-iganaika-zai-du-que-ren-sitekudasai-zai-du-miip-misitekudasai
const result = security.string.safe(
'に間違いがないか、再度確認してください。再読み込みしてください。',
options,
);
assert.equal(
result,
'nijian-wei-iganaika-zai-du-que-ren-sitekudasai-zai-du-miip-misitekudasai',
'ni-jian-weiiganaika-zai-du-que-renshitekudasai-zai-dumi-yumishitekudasai',
);
});

it('should not transliterate the slugs if the unicodeSlugs flag is passed', function () {
let result = security.string.safe('Ett smörgåsbord från Sydkorea: 스뫼르고스보르드', {
unicodeSlugs: true,
});
assert.equal(result, 'ett-smörgåsbord-från-sydkorea-스뫼르고스보르드');
});

it('should not replace existing dashes and underscores when the slugSeparator is set to spaces', function () {
let result = security.string.safe('Herr./Klaus-Jürgen_44', {
slugSeparator: ' ',
});
assert.equal(result, 'herr klaus-jurgen_44');
});

it('should not lose or convert dashes if options are passed with truthy importing flag', function () {
let result = security.string.safe('-slug-with-starting-ending-and---multiple-dashes-', {
importing: true,
Expand All @@ -104,7 +121,7 @@ describe('Lib: Security - String', function () {
let result = security.string.safe("-slug-&with-✓-invalid-characters-に'", {
importing: true,
});
assert.equal(result, '-slug--with--invalid-characters-ni');
assert.equal(result, '-slug--with---invalid-characters-ni');
});
});
});
6 changes: 5 additions & 1 deletion packages/validator/lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ validators.isEmptyOrURL = function isEmptyOrURL(str) {

validators.isSlug = function isSlug(str) {
assertString(str);
return validators.matches(str, /^[a-z0-9\-_]+$/);
// The slugs should always be normalized with NFC before being used, but some languages rely on
// combining marks to create letters. To avoid misuse, the slugify() function only generates slugs
// with a natural number of combining marks. Marks in the beginning of a word means they're invalid,
// and in the rest of the slug a maximum of three combining marks is permitted to each letter.
return validators.matches(str, /^(?:[\p{L}\p{N}][\p{Mn}\p{Mc}]{0,3}|[ _-])+$/u);
};

validators.isEmail = function isEmail(str, options = { legacy: true }) {
Expand Down
30 changes: 29 additions & 1 deletion packages/validator/test/internals.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,35 @@ describe('Validator internals', function () {

it('isSlug validates slug format', function () {
assert.equal(validator.isSlug('a-valid_slug-1'), true);
assert.equal(validator.isSlug('not valid slug'), false);
assert.equal(validator.isSlug('not? valid slug'), false);
});

it('isSlug accepts slugs generated without optional changes', function () {
assert.equal(validator.isSlug('-separator-in-beginning'), true);
assert.equal(validator.isSlug('-separator-in-beginning-and-end-'), true);
assert.equal(validator.isSlug('multiple---separators-in--use'), true);
assert.equal(validator.isSlug('different__kinds-of separator___in-the-same-text'), true);
});

it('isSlug rejects slugs with too many combining marks, but accept some of them', function () {
// note that this might break some text editors, so it might need to be removed
assert.equal(validator.isSlug('ghost-blo̵̯͝ǵ'), true);
assert.equal(validator.isSlug('G̸̛̦̼̜̱̹̦̲̩̰̀̓̆̇̔̎̒̎h̸͕̹̤̿͌́͊͋̈̂͗̕o̶̠͑̍s̷̝̭̰̳̖̣͉̈́̌̐́̈́̒͂̚t̴̩̦̫̟̲̘̆̔̑̅͘̕͠͝͠ ̶̜̺͚̆̈ͅb̸̰͕͔͈̤̾̉͒̂̎ͅl̵̳͚̘̯̀̎o̵̯͝ǵ̴̨̛͍̞͙̲̦̗̖͍̂̈́͆͝'), false);
});

it('isSlug rejects slugs with combining marks in the beginning of a text or a word', function () {
// note that this might break some text editors, so it might need to be removed
assert.equal(validator.isSlug('ผีในวัฒนธรรมไทย'), true);
assert.equal(validator.isSlug('\u0303\u0301\u0302ผีในวัฒนธรรมไทย'), false);
assert.equal(validator.isSlug('ghost-\u0301\u0302blog'), false);
});

it('isSlug accepts foreign characters, in this case Japanese', function () {
assert.equal(validator.isSlug('に間違いがないか_再度確認してください_再読み込みしてください'), true);
});

it('isSlug rejects non-letter characters, like emojis', function () {
assert.equal(validator.isSlug('im-so-happy-today-🙃'), false);
});

it('custom validators enforce string input', function () {
Expand Down