|
|
@@ -958,16 +958,26 @@ export function getDocid(hash: string): string {
|
|
|
* - Preserve folder structure (a/b/c/d.md stays structured)
|
|
|
* - Preserve file extension
|
|
|
*/
|
|
|
+/** Replace emoji/symbol codepoints with their hex representation (e.g. 🐘 → 1f418) */
|
|
|
+function emojiToHex(str: string): string {
|
|
|
+ return str.replace(/(?:\p{So}\p{Mn}?|\p{Sk})+/gu, (run) => {
|
|
|
+ // Split the run into individual emoji and convert each to hex, dash-separated
|
|
|
+ return [...run].filter(c => /\p{So}|\p{Sk}/u.test(c))
|
|
|
+ .map(c => c.codePointAt(0)!.toString(16)).join('-');
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
export function handelize(path: string): string {
|
|
|
if (!path || path.trim() === '') {
|
|
|
throw new Error('handelize: path cannot be empty');
|
|
|
}
|
|
|
|
|
|
// Allow route-style "$" filenames while still rejecting paths with no usable content.
|
|
|
+ // Emoji (\p{So}) counts as valid content — they get converted to hex codepoints below.
|
|
|
const segments = path.split('/').filter(Boolean);
|
|
|
const lastSegment = segments[segments.length - 1] || '';
|
|
|
const filenameWithoutExt = lastSegment.replace(/\.[^.]+$/, '');
|
|
|
- const hasValidContent = /[\p{L}\p{N}$]/u.test(filenameWithoutExt);
|
|
|
+ const hasValidContent = /[\p{L}\p{N}\p{So}\p{Sk}$]/u.test(filenameWithoutExt);
|
|
|
if (!hasValidContent) {
|
|
|
throw new Error(`handelize: path "${path}" has no valid filename content`);
|
|
|
}
|
|
|
@@ -979,6 +989,9 @@ export function handelize(path: string): string {
|
|
|
.map((segment, idx, arr) => {
|
|
|
const isLastSegment = idx === arr.length - 1;
|
|
|
|
|
|
+ // Convert emoji to hex codepoints before cleaning
|
|
|
+ segment = emojiToHex(segment);
|
|
|
+
|
|
|
if (isLastSegment) {
|
|
|
// For the filename (last segment), preserve the extension
|
|
|
const extMatch = segment.match(/(\.[a-z0-9]+)$/i);
|