Skip to content

Commit 334fdaf

Browse files
feat: move string utility methods from onedrive-support (#1033)
1 parent 1c979b8 commit 334fdaf

2 files changed

Lines changed: 237 additions & 1 deletion

File tree

packages/helix-shared-string/src/string.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,101 @@ export function multiline(str) {
5959
.map((l) => l.slice(prefixLen)) // discard prefixes
6060
.join('\n');
6161
}
62+
63+
/**
64+
* Splits the given name at the last '.', returning the extension and the base name.
65+
* @param {string} name Filename
66+
* @returns {string[]} Returns an array containing the base name and extension.
67+
*/
68+
export function splitByExtension(name) {
69+
const idx = name.lastIndexOf('.');
70+
const baseName = idx > 0 && idx < name.length - 1 ? name.substring(0, idx) : name;
71+
const ext = idx > 0 && idx < name.length - 1 ? name.substring(idx + 1).toLowerCase() : '';
72+
return [baseName, ext];
73+
}
74+
75+
/**
76+
* Sanitizes the given string by :
77+
* - convert to lower case
78+
* - normalize all unicode characters
79+
* - replace all non-alphanumeric characters with a dash
80+
* - remove all consecutive dashes
81+
* - remove all leading and trailing dashes
82+
*
83+
* @param {string} name
84+
* @returns {string} sanitized name
85+
*/
86+
export function sanitizeName(name) {
87+
return name
88+
.toLowerCase()
89+
.normalize('NFD')
90+
.replace(/[\u0300-\u036f]/g, '')
91+
.replace(/[^a-z0-9]+/g, '-')
92+
.replace(/^-|-$/g, '');
93+
}
94+
95+
/**
96+
* Sanitizes the file path by:
97+
* - convert to lower case
98+
* - normalize all unicode characters
99+
* - replace all non-alphanumeric characters with a dash
100+
* - remove all consecutive dashes
101+
* - remove all leading and trailing dashes
102+
*
103+
* Note that only the basename of the file path is sanitized. i.e. The ancestor path and the
104+
* extension is not affected.
105+
*
106+
* @param {string} filepath the file path
107+
* @param {object} opts Options
108+
* @param {boolean} [opts.ignoreExtension] if {@code true} ignores the extension
109+
* @returns {string} sanitized file path
110+
*/
111+
export function sanitizePath(filepath, opts = {}) {
112+
const idx = filepath.lastIndexOf('/') + 1;
113+
const extIdx = opts.ignoreExtension ? -1 : filepath.lastIndexOf('.');
114+
const pfx = filepath.substring(0, idx);
115+
const basename = extIdx < idx ? filepath.substring(idx) : filepath.substring(idx, extIdx);
116+
const ext = extIdx < idx ? '' : filepath.substring(extIdx);
117+
const name = sanitizeName(basename);
118+
return `${pfx}${name}${ext}`;
119+
}
120+
121+
/**
122+
* Compute the edit distance using a recursive algorithm. since we only expect to have relative
123+
* short filenames, the algorithm shouldn't be too expensive.
124+
*
125+
* @param {string} s0 Input string
126+
* @param {string} s1 Input string
127+
* @returns {number|*}
128+
*/
129+
export function editDistance(s0, s1) {
130+
// make sure that s0 length is greater than s1 length
131+
if (s0.length < s1.length) {
132+
const t = s1;
133+
// eslint-disable-next-line no-param-reassign
134+
s1 = s0;
135+
// eslint-disable-next-line no-param-reassign
136+
s0 = t;
137+
}
138+
const l0 = s0.length;
139+
const l1 = s1.length;
140+
141+
// init first row
142+
const resultMatrix = [[]];
143+
for (let c = 0; c < l1 + 1; c += 1) {
144+
resultMatrix[0][c] = c;
145+
}
146+
// fill out the distance matrix and find the best path
147+
for (let i = 1; i < l0 + 1; i += 1) {
148+
resultMatrix[i] = [i];
149+
for (let j = 1; j < l1 + 1; j += 1) {
150+
const replaceCost = (s0.charAt(i - 1) === s1.charAt(j - 1)) ? 0 : 1;
151+
resultMatrix[i][j] = Math.min(
152+
resultMatrix[i - 1][j] + 1, // insert
153+
resultMatrix[i][j - 1] + 1, // remove
154+
resultMatrix[i - 1][j - 1] + replaceCost,
155+
);
156+
}
157+
}
158+
return resultMatrix[l0][l1];
159+
}

packages/helix-shared-string/test/string.test.js

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
/* eslint-env mocha */
1414

1515
import assert from 'assert';
16-
import { multiline } from '../src/string.js';
16+
import {
17+
multiline, editDistance, sanitizeName, sanitizePath, splitByExtension,
18+
} from '../src/string.js';
1719

1820
describe('String tests', () => {
1921
it('multiline()', () => {
@@ -39,3 +41,139 @@ describe('String tests', () => {
3941
`);
4042
});
4143
});
44+
45+
describe('splitByExtension Tests', () => {
46+
it('extension split works for empty string', () => {
47+
assert.deepStrictEqual(['', ''], splitByExtension(''));
48+
});
49+
50+
it('extension split works for string w/o extension', () => {
51+
assert.deepStrictEqual(['foo', ''], splitByExtension('foo'));
52+
});
53+
54+
it('extension split works for string with extension', () => {
55+
assert.deepStrictEqual(['foo', 'txt'], splitByExtension('foo.txt'));
56+
});
57+
58+
it('extension split works for string with dots and extension', () => {
59+
assert.deepStrictEqual(['foo.bar', 'txt'], splitByExtension('foo.bar.txt'));
60+
});
61+
62+
it('extension split works for string ending with a dot', () => {
63+
assert.deepStrictEqual(['foo.', ''], splitByExtension('foo.'));
64+
});
65+
66+
it('extension split works for string starting with a dot', () => {
67+
assert.deepStrictEqual(['.foo', ''], splitByExtension('.foo'));
68+
});
69+
});
70+
71+
describe('sanitize Tests', () => {
72+
it('sanitize works for empty string', () => {
73+
assert.strictEqual(sanitizeName(''), '');
74+
});
75+
76+
it('sanitize transform string to lower case', () => {
77+
assert.strictEqual(sanitizeName('MyDocument'), 'mydocument');
78+
});
79+
80+
it('sanitize transforms non-alpha to dashes', () => {
81+
assert.strictEqual(sanitizeName('My 2. Document'), 'my-2-document');
82+
});
83+
84+
it('sanitize removes leading dashes', () => {
85+
assert.strictEqual(sanitizeName('.My 2. Document'), 'my-2-document');
86+
});
87+
88+
it('sanitize removes trailing dashes', () => {
89+
assert.strictEqual(sanitizeName('.My 2. Document-'), 'my-2-document');
90+
});
91+
92+
it('sanitize normalizes unicode', () => {
93+
assert.strictEqual(sanitizeName('Föhren Smürd'), 'fohren-smurd');
94+
});
95+
});
96+
97+
describe('editDistance Tests', () => {
98+
it('editDistances works for empty strings', () => {
99+
assert.strictEqual(0, editDistance('', ''));
100+
});
101+
102+
it('editDistances works for equal strings', () => {
103+
assert.strictEqual(0, editDistance('foo', 'foo'));
104+
});
105+
106+
it('editDistances works for appended characters', () => {
107+
assert.strictEqual(3, editDistance('foo', 'foo123'));
108+
});
109+
110+
it('editDistances works for removed characters from the end', () => {
111+
assert.strictEqual(3, editDistance('foo123', 'foo'));
112+
});
113+
114+
it('editDistances works for replaced characters', () => {
115+
assert.strictEqual(3, editDistance('My Document', 'my-document'));
116+
});
117+
118+
it('editDistances works for more complicate replacements', () => {
119+
assert.strictEqual(5, editDistance('My 1. Document', 'my-1-document'));
120+
});
121+
122+
it('editDistances works for more complicate replacements (2)', () => {
123+
assert.strictEqual(10, editDistance('my-1-document', 'My 1. Document.docx'));
124+
});
125+
126+
it('editDistances is reasonably fast for long names)', () => {
127+
const t0 = Date.now();
128+
assert.strictEqual(66, editDistance(
129+
'my-1-document my-1-document my-1-document my-1-document my-1-document my-1-document my-1-document my-1-document my-1-document my-1-document my-1-document my-1-document ',
130+
'My 1. Document My 1. Document My 1. Document My 1. Document My 1. Document My 1. Document My 1. Document My 1. Document My 1. Document My 1. Document My 1. Document .docx',
131+
));
132+
const t1 = Date.now();
133+
assert.ok(t1 - t0 < 100);
134+
});
135+
});
136+
137+
describe('sanitizePath Tests', () => {
138+
it('sanitizePath works for empty string', () => {
139+
assert.strictEqual(sanitizePath(''), '');
140+
});
141+
142+
it('sanitizePath transform string to lower case', () => {
143+
assert.strictEqual(sanitizePath('MyDocument'), 'mydocument');
144+
});
145+
146+
it('sanitizePath can ignore extension', () => {
147+
assert.strictEqual(sanitizePath('.MyDocument', {
148+
ignoreExtension: true,
149+
}), 'mydocument');
150+
});
151+
152+
it('sanitizePath works with dots in path and no extension', () => {
153+
assert.strictEqual(sanitizePath('/foo.bar/My Document'), '/foo.bar/my-document');
154+
});
155+
156+
it('sanitizePath only transforms last path segment', () => {
157+
assert.strictEqual(sanitizePath('/Untitled Folder/MyDocument'), '/Untitled Folder/mydocument');
158+
});
159+
160+
it('sanitizePath only transforms root segment', () => {
161+
assert.strictEqual(sanitizePath('/MyDocument'), '/mydocument');
162+
});
163+
164+
it('sanitizePath transforms non-alpha to dashes', () => {
165+
assert.strictEqual(sanitizePath('My 2. Document.docx'), 'my-2-document.docx');
166+
});
167+
168+
it('sanitizePath removes leading dashes', () => {
169+
assert.strictEqual(sanitizePath('.My 2. Document.docx'), 'my-2-document.docx');
170+
});
171+
172+
it('sanitizePath removes trailing dashes', () => {
173+
assert.strictEqual(sanitizePath('.My 2. Document!.docx'), 'my-2-document.docx');
174+
});
175+
176+
it('sanitizePath normalizes unicode', () => {
177+
assert.strictEqual(sanitizePath('Föhren Smürd'), 'fohren-smurd');
178+
});
179+
});

0 commit comments

Comments
 (0)