textconvert
Version:
Public library to convert text into many conventions and formats.
1,673 lines (1,662 loc) • 41.8 kB
JavaScript
/**
* Make a regex list to split words based on these patterns
*/
const regex = {
values: {
upperCaseKeepLetter: /(?=[A-Z])/g,
nonAlphabetic: /[^A-Za-z]/g,
nonAlphaTest: /[^A-Za-z]/,
punctuation: /[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/g,
},
};
const { values: values$1 } = regex;
/**
* Convert a string from any convention to Camel Case convention.
* @param text A string to be converted to Camel Case.
* @returns A string in camelCase convention.
* @example
* camelCase('hello world'); // 'helloWorld'
*/
function camelCase(text) {
var _a;
// Make sure there's an input
if (!text)
return 'Please provide a valid input text';
// Make an array of words after splitting them
const wordsArray = text.split(values$1.nonAlphabetic);
// Get the first word out of the array
const firstWord = (_a = wordsArray.shift()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
// convert the words to camelCase
const cCaseArray = wordsArray.map((word) => {
word = word.charAt(0).toUpperCase() + word.slice(1);
return word;
});
// Join the words and return them
return firstWord + cCaseArray.join('');
}
/**
* Convert a string from any convention to Pascal Case convention.
* @param text A string to be converted to Camel Case.
* @returns A string in PascalCase convention.
* @example
* pascalCase('hello world'); // 'HelloWorld'
*/
function pascalCase(text) {
// Make sure there's an input
if (!text)
return 'Please provide a valid input text';
// Make an array of words after splitting them
const wordsArray = text.split(values$1.nonAlphabetic);
// convert the words to camelCase
const pCaseArray = wordsArray.map((word) => {
word = word.charAt(0).toUpperCase() + word.slice(1);
return word;
});
// Join the words and return them
return pCaseArray.join('');
}
/**
* Convert a string from any convention to Snake Case convention.
* @param text A string to be converted to Snake Case.
* @returns A string in snake_case convention.
* @example
* snakeCase('hello world'); // 'hello_world'
*/
function snakeCase(text) {
// Make sure there's an input
if (!text)
return 'Please provide a valid input text';
// Make an array of words after splitting them depending on the input case
const wordsArray = values$1.nonAlphaTest.test(text)
? text.split(values$1.nonAlphabetic)
: text.split(values$1.upperCaseKeepLetter);
// Filter the words to 1 letter minimum length and convert the words to lowerCase
const sCaseArray = wordsArray
.filter((word) => word.length > 0)
.map((word) => {
word = word.charAt(0).toLowerCase() + word.slice(1);
return word;
});
// Join the words with "_" and return them
return sCaseArray.join('_');
}
/**
* Convert a string from any convention to Kebab Case convention.
* @param text A string to be converted to Kebab Case.
* @returns A string in kebab-case convention.
* @example
* kebabCase('hello world'); // 'hello-world'
*/
function kebabCase(text) {
// Make sure there's an input
if (!text)
return 'Please provide a valid input text';
// Make an array of words after splitting them depending on the input case
const wordsArray = values$1.nonAlphaTest.test(text)
? text.split(values$1.nonAlphabetic)
: text.split(values$1.upperCaseKeepLetter);
// Filter the words to 1 letter minimum length and convert the words to lowerCase
const kCaseArray = wordsArray
.filter((word) => word.length > 0)
.map((word) => {
word = word.charAt(0).toLowerCase() + word.slice(1);
return word;
});
// Join the words with "-" and return them
return kCaseArray.join('-');
}
/**
* Supported languages for detection
*/
var Language;
(function (Language) {
Language["English"] = "english";
Language["French"] = "french";
Language["Spanish"] = "spanish";
Language["German"] = "german";
Language["Italian"] = "italian";
Language["Portuguese"] = "portuguese";
Language["Dutch"] = "dutch";
Language["Unknown"] = "unknown";
})(Language || (Language = {}));
// Language profiles (character frequency data)
const languageProfiles = {
[Language.English]: {
e: 12.7,
t: 9.1,
a: 8.2,
o: 7.5,
i: 7.0,
n: 6.7,
s: 6.3,
h: 6.1,
r: 6.0,
d: 4.3,
l: 4.0,
u: 2.8,
c: 2.8,
m: 2.4,
w: 2.4,
f: 2.2,
g: 2.0,
y: 2.0,
p: 1.9,
b: 1.5,
v: 1.0,
k: 0.8,
j: 0.2,
x: 0.2,
q: 0.1,
z: 0.1,
th: 3.0,
he: 2.5,
in: 2.0,
er: 1.8,
an: 1.6,
re: 1.4,
on: 1.3,
at: 1.2,
},
[Language.French]: {
e: 14.7,
a: 8.0,
i: 7.5,
s: 7.9,
n: 7.1,
t: 7.0,
r: 6.5,
l: 5.5,
u: 6.0,
o: 5.3,
d: 3.5,
c: 3.0,
m: 2.6,
p: 2.5,
v: 1.6,
h: 0.8,
g: 1.0,
f: 1.0,
b: 0.9,
q: 1.3,
j: 0.3,
z: 0.1,
x: 0.4,
k: 0.0,
w: 0.0,
y: 0.3,
é: 1.9,
è: 0.3,
ê: 0.2,
à: 0.5,
â: 0.1,
ç: 0.3,
ai: 1.0,
oi: 0.4,
ou: 0.9,
},
[Language.Spanish]: {
e: 13.7,
a: 12.5,
o: 8.7,
s: 7.8,
n: 7.0,
r: 6.4,
i: 6.2,
l: 5.8,
d: 5.8,
t: 4.6,
c: 4.0,
u: 3.9,
m: 3.1,
p: 2.5,
b: 1.4,
g: 1.0,
v: 1.0,
f: 0.7,
h: 0.7,
q: 0.9,
j: 0.4,
z: 0.5,
ñ: 0.3,
y: 1.0,
x: 0.2,
k: 0.0,
w: 0.0,
á: 0.5,
é: 0.4,
í: 0.4,
ó: 0.9,
ú: 0.4,
qu: 0.8,
ue: 0.6,
ch: 0.4,
},
[Language.German]: {
e: 16.9,
n: 10.0,
i: 7.6,
s: 7.3,
r: 7.0,
t: 6.1,
a: 6.5,
d: 5.1,
h: 4.8,
u: 4.4,
l: 3.4,
c: 2.7,
g: 3.0,
m: 2.5,
o: 2.5,
b: 1.9,
w: 1.9,
f: 1.7,
k: 1.4,
z: 1.1,
v: 0.7,
p: 0.7,
j: 0.3,
y: 0.1,
q: 0.02,
x: 0.03,
ä: 0.5,
ö: 0.3,
ü: 0.6,
ß: 0.3,
ch: 2.6,
ei: 1.8,
en: 3.8,
er: 3.6,
ie: 1.7,
},
[Language.Italian]: {
e: 11.8,
a: 11.7,
i: 10.1,
o: 9.8,
n: 6.9,
t: 5.6,
r: 6.4,
l: 6.3,
s: 5.0,
c: 4.5,
d: 3.7,
p: 3.0,
u: 3.0,
m: 2.5,
v: 2.1,
g: 1.6,
h: 1.5,
b: 0.9,
f: 0.9,
z: 0.5,
q: 0.5,
à: 0.6,
è: 0.4,
ì: 0.1,
ò: 0.2,
ù: 0.1,
ch: 0.5,
di: 0.9,
la: 0.7,
re: 0.6,
},
[Language.Portuguese]: {
a: 14.6,
e: 12.6,
o: 10.7,
s: 7.8,
r: 6.5,
i: 6.2,
n: 5.0,
t: 4.7,
d: 4.9,
m: 4.7,
u: 4.0,
c: 3.7,
l: 2.8,
p: 2.5,
v: 1.7,
g: 1.3,
b: 1.0,
f: 1.0,
h: 0.8,
q: 1.2,
ã: 0.7,
á: 0.5,
é: 0.4,
ó: 0.4,
ç: 0.5,
õ: 0.3,
j: 0.3,
z: 0.5,
ê: 0.1,
â: 0.1,
qu: 0.8,
de: 0.7,
es: 0.7,
},
[Language.Dutch]: {
e: 19.0,
n: 10.0,
a: 7.5,
t: 6.5,
i: 6.5,
r: 6.0,
o: 6.0,
d: 5.8,
s: 3.7,
l: 3.5,
g: 3.4,
v: 2.8,
h: 2.4,
k: 2.2,
m: 2.2,
u: 2.0,
j: 1.5,
w: 1.5,
z: 1.4,
p: 1.2,
b: 1.6,
c: 1.2,
f: 0.8,
y: 0.0,
x: 0.0,
q: 0.0,
ij: 1.0,
en: 3.5,
de: 3.0,
er: 2.0,
ee: 1.8,
oo: 1.0,
},
[Language.Unknown]: {},
};
// Common stopwords for each language
const stopwords = {
[Language.English]: [
'the',
'be',
'to',
'of',
'and',
'a',
'in',
'that',
'have',
'i',
'it',
'for',
'not',
'on',
'with',
'he',
'as',
'you',
'do',
'at',
'this',
'but',
'his',
'by',
'from',
'they',
'we',
'say',
'her',
'she',
'or',
'an',
'will',
'my',
'one',
'all',
'would',
'there',
'their',
'what',
'so',
'up',
'out',
'if',
'about',
'who',
'get',
'which',
'go',
'me',
'when',
'make',
'can',
'like',
'time',
'no',
'just',
'him',
'know',
'take',
'people',
'into',
'year',
'your',
'good',
'some',
'could',
'them',
'see',
'other',
'than',
'then',
'now',
'look',
'only',
'come',
'its',
'over',
'think',
'also',
'back',
'after',
'use',
'two',
'how',
'our',
'work',
'first',
'well',
'way',
'even',
'new',
'want',
'because',
'any',
'these',
'give',
'day',
'most',
'us',
'is',
'am',
'are',
'was',
'were',
'been',
'has',
'had',
'would',
'should',
'could',
'hello',
'world',
],
[Language.French]: [
'le',
'la',
'les',
'un',
'une',
'des',
'et',
'est',
'en',
'que',
'qui',
'dans',
'à',
'pour',
'pas',
'de',
'ce',
'il',
'elle',
'je',
'nous',
'vous',
'ils',
'elles',
'son',
'sa',
'ses',
'mon',
'ma',
'mes',
'ton',
'ta',
'tes',
'notre',
'votre',
'leur',
'leurs',
'du',
'au',
'aux',
'sur',
'sous',
'avec',
'sans',
'mais',
'ou',
'où',
'donc',
'car',
'comme',
'comment',
'quand',
'pourquoi',
'parce',
'plus',
'moins',
'très',
'trop',
'peu',
'aussi',
'bien',
'mal',
'si',
'tout',
'tous',
'toute',
'toutes',
'autre',
'autres',
'même',
'ici',
'là',
'cela',
'ceci',
'celui',
'celle',
'ceux',
'celles',
'rien',
'personne',
'quelque',
'quelques',
'plusieurs',
'beaucoup',
'être',
'avoir',
'faire',
'dire',
'venir',
'voir',
'vouloir',
'pouvoir',
'bonjour',
'monde',
],
[Language.Spanish]: [
'el',
'la',
'los',
'las',
'un',
'una',
'unos',
'unas',
'y',
'o',
'que',
'en',
'de',
'a',
'por',
'con',
'no',
'es',
'son',
'para',
'como',
'su',
'sus',
'mi',
'mis',
'tu',
'tus',
'nuestro',
'nuestra',
'nuestros',
'nuestras',
'vuestro',
'vuestra',
'vuestros',
'vuestras',
'del',
'al',
'este',
'esta',
'estos',
'estas',
'ese',
'esa',
'esos',
'esas',
'aquel',
'aquella',
'aquellos',
'aquellas',
'pero',
'más',
'menos',
'mucho',
'muchos',
'mucha',
'muchas',
'poco',
'pocos',
'poca',
'pocas',
'algún',
'alguna',
'algunos',
'algunas',
'ningún',
'ninguna',
'ningunos',
'ningunas',
'otro',
'otra',
'otros',
'otras',
'mismo',
'misma',
'mismos',
'mismas',
'tan',
'tanto',
'tanta',
'tantos',
'tantas',
'así',
'también',
'solo',
'solamente',
'hola',
'mundo',
],
[Language.German]: [
'der',
'die',
'das',
'ein',
'eine',
'einen',
'dem',
'den',
'des',
'einer',
'eines',
'und',
'ist',
'sind',
'war',
'waren',
'wird',
'werden',
'in',
'zu',
'mit',
'für',
'von',
'auf',
'als',
'um',
'an',
'aus',
'wie',
'bei',
'nach',
'bis',
'seit',
'vor',
'durch',
'über',
'unter',
'gegen',
'ohne',
'dass',
'weil',
'wenn',
'aber',
'oder',
'nur',
'noch',
'schon',
'auch',
'selbst',
'mir',
'dir',
'ihm',
'ihr',
'uns',
'euch',
'ihnen',
'mein',
'dein',
'sein',
'ihr',
'unser',
'euer',
'ihre',
'nicht',
'kein',
'keine',
'hallo',
'welt',
],
[Language.Italian]: [
'il',
'lo',
'la',
'i',
'gli',
'le',
'un',
'uno',
'una',
'e',
'è',
'che',
'di',
'a',
'per',
'in',
'con',
'su',
'non',
'sono',
'ho',
'mi',
'ha',
'si',
'ti',
'ci',
'vi',
'lo',
'la',
'li',
'le',
'ne',
'mio',
'mia',
'miei',
'mie',
'tuo',
'tua',
'tuoi',
'tue',
'suo',
'sua',
'suoi',
'sue',
'nostro',
'nostra',
'nostri',
'nostre',
'vostro',
'vostra',
'vostri',
'vostre',
'loro',
'questo',
'questa',
'questi',
'queste',
'quello',
'quella',
'quelli',
'quelle',
'ma',
'più',
'meno',
'molto',
'molti',
'molta',
'molte',
'poco',
'pochi',
'poca',
'poche',
'grande',
'grandi',
'piccolo',
'piccoli',
'piccola',
'piccole',
'anche',
'ciao',
'mondo',
],
[Language.Portuguese]: [
'o',
'a',
'os',
'as',
'um',
'uma',
'uns',
'umas',
'e',
'é',
'que',
'de',
'em',
'para',
'com',
'não',
'por',
'se',
'na',
'do',
'da',
'dos',
'das',
'no',
'nas',
'ao',
'aos',
'à',
'às',
'pelo',
'pela',
'pelos',
'pelas',
'este',
'esta',
'estes',
'estas',
'esse',
'essa',
'esses',
'essas',
'aquele',
'aquela',
'aqueles',
'aquelas',
'isto',
'isso',
'aquilo',
'meu',
'minha',
'meus',
'minhas',
'teu',
'tua',
'teus',
'tuas',
'seu',
'sua',
'seus',
'suas',
'nosso',
'nossa',
'nossos',
'nossas',
'vosso',
'vossa',
'vossos',
'vossas',
'dele',
'dela',
'deles',
'delas',
'nele',
'nela',
'neles',
'nelas',
'olá',
'mundo',
],
[Language.Dutch]: [
'de',
'het',
'een',
'is',
'en',
'van',
'in',
'te',
'dat',
'op',
'voor',
'niet',
'met',
'zijn',
'worden',
'deze',
'dit',
'door',
'er',
'ook',
'als',
'aan',
'maar',
'bij',
'nog',
'om',
'uit',
'zo',
'dan',
'over',
'na',
'toen',
'tot',
'werd',
'wel',
'nu',
'je',
'jij',
'jou',
'jouw',
'ik',
'mij',
'mijn',
'hij',
'hem',
'zijn',
'zij',
'haar',
'we',
'wij',
'ons',
'onze',
'jullie',
'hun',
'hen',
'hallo',
'wereld',
],
[Language.Unknown]: [],
};
// Common English words that should strongly bias toward English
const commonEnglishWords = new Set([
'hello',
'world',
'the',
'this',
'that',
'there',
'their',
'they',
'them',
'then',
'and',
'but',
'or',
'not',
'what',
'when',
'where',
'who',
'why',
'how',
'all',
'any',
'every',
'some',
'many',
'much',
'few',
'little',
'other',
'another',
'such',
'even',
'only',
'just',
'also',
'very',
'too',
'quite',
'rather',
'enough',
]);
// Cache for previously analyzed texts
const resultCache = new Map();
// Normalizes a language profile for cosine similarity
const normalizedProfiles = Object.entries(languageProfiles).reduce((acc, [lang, profile]) => {
if (lang !== Language.Unknown) {
// Calculate magnitude for normalization
const entries = Object.entries(profile);
const magnitude = Math.sqrt(entries.reduce((sum, [, value]) => sum + value * value, 0));
// Create normalized map
const normalizedMap = new Map();
entries.forEach(([char, value]) => {
normalizedMap.set(char, value / magnitude);
});
acc[lang] = normalizedMap;
}
else {
acc[Language.Unknown] = new Map();
}
return acc;
}, {});
// Handle very common language expressions directly
const commonPhrases = new Map([
// English phrases
['hello world', Language.English],
['hello', Language.English],
['hi there', Language.English],
['good morning', Language.English],
['good evening', Language.English],
['good night', Language.English],
['thank you', Language.English],
['how are you', Language.English],
['nice to meet you', Language.English],
// Spanish phrases
['hola mundo', Language.Spanish],
['hola', Language.Spanish],
['buenos días', Language.Spanish],
['buenas tardes', Language.Spanish],
['buenas noches', Language.Spanish],
['gracias', Language.Spanish],
['cómo estás', Language.Spanish],
['mucho gusto', Language.Spanish],
// German phrases
['hallo welt', Language.German],
['hallo', Language.German],
['guten morgen', Language.German],
['guten tag', Language.German],
['guten abend', Language.German],
['danke', Language.German],
['wie geht es dir', Language.German],
// French phrases
['bonjour le monde', Language.French],
['bonjour', Language.French],
['bonsoir', Language.French],
['merci', Language.French],
['comment allez-vous', Language.French],
['enchanté', Language.French],
// Italian phrases
['ciao mondo', Language.Italian],
['ciao', Language.Italian],
['buongiorno', Language.Italian],
['buonasera', Language.Italian],
['grazie', Language.Italian],
['come stai', Language.Italian],
['piacere', Language.Italian],
// Portuguese phrases
['olá mundo', Language.Portuguese],
['olá', Language.Portuguese],
['bom dia', Language.Portuguese],
['boa tarde', Language.Portuguese],
['boa noite', Language.Portuguese],
['obrigado', Language.Portuguese],
['como vai', Language.Portuguese],
['prazer em conhecê-lo', Language.Portuguese],
// Dutch phrases
['hallo wereld', Language.Dutch],
['hallo', Language.Dutch],
['goedemorgen', Language.Dutch],
['goedemiddag', Language.Dutch],
['goedenavond', Language.Dutch],
['dank je', Language.Dutch],
['hoe gaat het', Language.Dutch],
]);
// Special handling for languages with unique characters
const uniqueChars = {
[Language.German]: ['ä', 'ö', 'ü', 'ß'],
[Language.French]: ['é', 'è', 'ê', 'ë', 'à', 'â', 'ù', 'û', 'î', 'ï', 'ô', 'œ', 'ç'],
[Language.Spanish]: ['ñ', 'á', 'é', 'í', 'ó', 'ú', 'ü', '¿', '¡'],
[Language.Italian]: ['à', 'è', 'é', 'ì', 'í', 'î', 'ò', 'ó', 'ù', 'ú'],
[Language.Portuguese]: ['ã', 'õ', 'á', 'à', 'â', 'é', 'ê', 'í', 'ó', 'ô', 'ú', 'ç'],
};
/**
* Detects the most likely language of a given text
*
* @param text The text to analyze
* @param minLength Minimum text length for reliable detection (default: 4)
* @param options Additional options for detection
* @returns Language detection result with confidence score
* @example
* detectLanguage('Bonjour le monde'); // { language: 'French', ... }
*/
function detectLanguage(text, minLength = 4, options = {
maxCharsToAnalyze: 500,
useCache: true,
}) {
const { maxCharsToAnalyze = 500, useCache = true } = options;
// Caching shortcut
if (useCache && resultCache.has(text)) {
return resultCache.get(text);
}
// Empty string case
if (!text || text.trim().length === 0) {
return {
language: Language.Unknown,
confidence: 0,
scores: Object.fromEntries(Object.values(Language).map((l) => [l, l === Language.Unknown ? 1 : 0])),
};
}
const lowerText = text.toLowerCase().trim();
// Phrase shortcut (exact match)
if (commonPhrases.has(lowerText)) {
const detected = commonPhrases.get(lowerText);
const result = {
language: detected,
confidence: 0.95,
scores: Object.fromEntries(Object.values(Language).map((l) => [
l,
l === detected ? 0.95 : 0.05 / (Object.values(Language).length - 1),
])),
};
if (useCache)
cacheResult(text, result);
return result;
}
const isShort = text.length < minLength;
const analyzedText = text.slice(0, maxCharsToAnalyze).toLowerCase().replace(/[0-9]/g, '');
const charCount = new Map();
let totalChars = 0;
const bigramSet = new Set([
'th',
'he',
'in',
'er',
'an',
'en',
'ch',
'de',
'ei',
'te',
'st',
'le',
'ou',
'qu',
'je',
'ai',
'ui',
'ie',
're',
'oo',
]);
for (let i = 0; i < analyzedText.length; i++) {
const char = analyzedText[i];
if (/[a-zäöüßàáâéèêëïîìíñóòôùúûç]/i.test(char)) {
charCount.set(char, (charCount.get(char) || 0) + 1);
totalChars++;
if (i < analyzedText.length - 1) {
const bigram = analyzedText.substring(i, i + 2);
if (bigramSet.has(bigram)) {
charCount.set(bigram, (charCount.get(bigram) || 0) + 0.5);
totalChars += 0.25;
}
}
}
}
if (totalChars < 2) {
return {
language: Language.Unknown,
confidence: 0.1,
scores: Object.fromEntries(Object.values(Language).map((l) => [
l,
l === Language.Unknown ? 0.9 : 0.1 / (Object.values(Language).length - 1),
])),
};
}
for (const [lang, chars] of Object.entries(uniqueChars)) {
for (const char of chars) {
if (analyzedText.includes(char)) {
charCount.set('unique_' + lang, (charCount.get('unique_' + lang) || 0) + 18);
totalChars += 3;
}
}
}
const words = analyzedText.split(/\s+/);
// Skip gibberish check if any language keyword is present
const languageKeywords = [
['english', Language.English],
['deutsch', Language.German],
['german', Language.German],
['français', Language.French],
['french', Language.French],
['español', Language.Spanish],
['spanish', Language.Spanish],
['italiano', Language.Italian],
['italian', Language.Italian],
['português', Language.Portuguese],
['portuguese', Language.Portuguese],
['nederlands', Language.Dutch],
['dutch', Language.Dutch],
];
const hasKeyword = languageKeywords.some(([kw]) => analyzedText.includes(kw));
// Gibberish check: if no stopwords from any language and more than 2 words, return unknown
if (!hasKeyword) {
const stopwordHits = Object.values(Language)
.filter((l) => l !== Language.Unknown)
.some((lang) => words.some((word) => stopwords[lang].includes(word)));
if (!stopwordHits && words.length > 2) {
return {
language: Language.Unknown,
confidence: 0,
scores: Object.fromEntries(Object.values(Language).map((l) => [l, l === Language.Unknown ? 1 : 0])),
};
}
}
for (const lang of Object.values(Language).filter((l) => l !== Language.Unknown)) {
for (const word of words) {
if (stopwords[lang].includes(word)) {
const boost = lang === Language.English && commonEnglishWords.has(word) ? 36 : 28;
charCount.set(`stopword_${lang}`, (charCount.get(`stopword_${lang}`) || 0) + boost);
totalChars += 3;
}
}
}
const keywords = [
['english', Language.English],
['deutsch', Language.German],
['german', Language.German],
['français', Language.French],
['french', Language.French],
['español', Language.Spanish],
['spanish', Language.Spanish],
['italiano', Language.Italian],
['italian', Language.Italian],
['português', Language.Portuguese],
['portuguese', Language.Portuguese],
['nederlands', Language.Dutch],
['dutch', Language.Dutch],
];
for (const [kw, lang] of keywords) {
if (analyzedText.includes(kw)) {
charCount.set(`keyword_${lang}`, (charCount.get(`keyword_${lang}`) || 0) + 70);
totalChars += 8;
}
}
const freqMap = normalizeVector(charCount);
const scores = {};
let maxScore = 0;
let detected = Language.Unknown;
for (const lang of Object.values(Language)) {
if (lang === Language.Unknown) {
scores[lang] = 0;
continue;
}
const profile = normalizedProfiles[lang];
let score = 0;
for (const [token, normFreq] of freqMap.entries()) {
if (token.startsWith(`unique_${lang}`))
score += normFreq * 2;
else if (token.startsWith(`stopword_${lang}`))
score += normFreq * 3;
else if (token.startsWith(`keyword_${lang}`))
score += normFreq * 5;
else if (profile.has(token))
score += normFreq * profile.get(token);
}
scores[lang] = score;
if (score > maxScore) {
maxScore = score;
detected = lang;
}
}
// Set gibberish threshold to 0.4
if (maxScore < 0.4) {
detected = Language.Unknown;
scores[Language.Unknown] = 1;
}
const sumScores = Object.values(scores).reduce((a, b) => a + b, 0);
let confidence = sumScores > 0 ? maxScore / sumScores : 0;
if (isShort)
confidence *= 0.8;
// For long texts, boost confidence
if (analyzedText.length > 40)
confidence = Math.min(confidence * 1.1, 1.0);
confidence =
confidence > 0.7 ? 0.95 : confidence > 0.55 ? 0.85 : confidence > 0.4 ? 0.7 : confidence;
const result = {
language: detected,
confidence,
scores,
};
if (useCache)
cacheResult(text, result);
return result;
}
// --- UTILS ---
function normalizeVector(map) {
const magnitude = Math.sqrt([...map.values()].reduce((sum, val) => sum + val * val, 0));
const normMap = new Map();
for (const [key, val] of map.entries()) {
normMap.set(key, val / magnitude);
}
return normMap;
}
function cacheResult(text, result) {
if (resultCache.size >= 100) {
const firstKey = resultCache.keys().next().value;
if (typeof firstKey === 'string') {
resultCache.delete(firstKey);
}
}
resultCache.set(text, result);
}
const { values } = regex;
/**
* Clear punctuations from a string and replaces it with a whitespace character or returns an array of strings.
*
* @param text String input to clear from punctuation.
* @returns Either a string or an array of strings cleared from punctuations based on the arguments passed.
* @example
* clear('Hello, world!'); // 'hello world'
*/
function clear(text) {
// Make sure there's an input
if (!text)
return 'Please provide a valid input text';
// Create an array of words
const wordsArray = text.split(values.punctuation);
// Filter wordsArray and remove punctuations
const clearWords = wordsArray.map((word) => {
word = word.trim().toLowerCase().replace(values.punctuation, '');
return word;
});
return clearWords.join(' ');
}
/**
* Return a boolean value number of the letters in a string.
*
* @param text String input to get letters count from.
* @param countNumbers boolean value to determine if numbers should be counted as letters.
*
* @returns Number of letters and numbers (if requested) in a string.
* @example
* count('Hello, world!'); // 10
* count('Hello0 world', true); // 11
*/
function count(text, countNumbers = false) {
// Check string length
if (!text.length)
return 0;
// Create a temp number.
let temp = 0;
// Clear the string and split the letters into an array.
const cleared = clear(text).split('');
// Loop through the letters array.
cleared.forEach((letter) => {
// Check if countNumbers is included
if (!countNumbers) {
// Count letters only
if (!(/[a-z]/g.test(letter) || /[A-Z]/g.test(letter))) {
return;
}
temp++;
return;
}
// Count letters with numbers.
if (!(/[a-z]/g.test(letter) || /[A-Z]/g.test(letter) || /[0-9]/g.test(letter)))
return;
temp++;
});
// Return the number of letters.
return temp;
}
/**
* Counts the number of words in a string.
* Words are defined as sequences of letters, numbers, and apostrophes separated by whitespace.
*
* @param text String input to count words from
* @returns Number of words in the string
* @example
* countWords('Hello, world!'); // 2
*/
function countWords(text) {
if (!(text === null || text === void 0 ? void 0 : text.trim()))
return 0;
// If the text contains only punctuation, return 0
if (!/[a-zA-Z0-9]/.test(text))
return 0;
return text
.trim()
.split(/\s+/)
.filter((word) => word.length > 0 && /[a-zA-Z0-9]/.test(word)).length;
}
/**
* Counts the number of sentences in a string.
* Sentences are defined as sequences of text ending with ., !, or ? followed by whitespace or end of string.
* Text without any sentence-ending punctuation is considered to be a single sentence.
*
* @param text String input to count sentences from
* @returns Number of sentences in the string
* @example
* countSentences('Hello world! How are you?'); // 2
*/
function countSentences(text) {
if (!(text === null || text === void 0 ? void 0 : text.trim()))
return 0;
// If there's no sentence-ending punctuation but there is text, it's considered one sentence
if (!/[.!?]/.test(text) && text.trim().length > 0)
return 1;
return text
.trim()
.split(/[.!?]+(?=\s|$)/)
.filter((sentence) => sentence.trim().length > 0).length;
}
/**
* Analyzes text and returns comprehensive statistics about it
*
* @param text The text to analyze
* @param wordsPerMinute Reading speed in words per minute (default: 200)
* @returns TextStatistics object with various metrics
* @example
* getTextStats('Hello world! This is a test.');
* // {
* // characterCount: 28,
* // characterCountNoSpaces: 24,
* // letterCount: 20,
* // alphanumericCount: 20,
* // wordCount: 6,
* // sentenceCount: 2,
* // paragraphCount: 1,
* // averageWordLength: 3.3,
* // averageSentenceLength: 3,
* // readingTimeSeconds: 2,
* // readingTimeFormatted: '2 sec'
* // }
*/
function getTextStats(text, wordsPerMinute = 200) {
// Handle empty input
if (!text || !text.trim()) {
return {
characterCount: 0,
characterCountNoSpaces: 0,
letterCount: 0,
alphanumericCount: 0,
wordCount: 0,
sentenceCount: 0,
paragraphCount: 0,
averageWordLength: 0,
averageSentenceLength: 0,
readingTimeSeconds: 0,
readingTimeFormatted: '0 sec',
};
}
// Basic counts
const characterCount = text.length;
const characterCountNoSpaces = text.replace(/\s/g, '').length;
const letterCount = count(text);
const alphanumericCount = count(text, true);
const wordCount = countWords(text);
const sentenceCount = countSentences(text);
// Paragraph count (separated by 2+ newlines)
const paragraphCount = text.split(/\n\s*\n/).filter((p) => p.trim().length > 0).length || 1;
// Calculate averages - fix for more accurate calculation
let totalWordLength = 0;
const words = text.split(/\s+/).filter((word) => word.length > 0);
for (const word of words) {
// Count only alphanumeric characters in words
totalWordLength += word.replace(/[^a-zA-Z0-9]/g, '').length;
}
const averageWordLength = wordCount > 0 ? Math.round((totalWordLength / wordCount) * 10) / 10 : 0;
const averageSentenceLength = sentenceCount > 0 ? Math.round((wordCount / sentenceCount) * 10) / 10 : 0;
// Calculate reading time
const wordsPerSecond = wordsPerMinute / 60;
const readingTimeSeconds = Math.round(wordCount / wordsPerSecond);
// Format reading time
const readingTimeFormatted = formatReadingTime(readingTimeSeconds);
return {
characterCount,
characterCountNoSpaces,
letterCount,
alphanumericCount,
wordCount,
sentenceCount,
paragraphCount,
averageWordLength,
averageSentenceLength,
readingTimeSeconds,
readingTimeFormatted,
};
}
/**
* Formats reading time in seconds to a readable string
*
* @param seconds Total seconds
* @returns Formatted string (e.g., "2 min 30 sec")
*/
function formatReadingTime(seconds) {
if (seconds === 0)
return '0 sec';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes === 0) {
return `${remainingSeconds} sec`;
}
else if (remainingSeconds === 0) {
return `${minutes} min`;
}
else {
return `${minutes} min ${remainingSeconds} sec`;
}
}
/**
* Reverses all characters in a string.
* @param text A string to reverse.
* @returns Reversed string.
* @example
* reverse('Hello, world!'); // '!dlrow ,olleH'
*/
function reverse(text) {
// return type is inferred
// Make sure input is valid
if (!text)
return 'Please provide a valid input text';
// Split string into characters, reverse all of them and join them back together
return text.split('').reverse().join('');
}
/**
* Returns an array of characters from the provided string.
* @param text A string to spread.
* @param clear Whether to clear punctuation from the text. Default is false.
* @returns Array of characters or an error message.
* @example
* spread('Hello, world!'); // ['H', 'e', ...]
* spread('Hello, world!', true); // ['H', 'e', ...]
*/
function spread(text, clear$1 = false) {
// Check if clearing punctuation is necessary.
if (clear$1) {
text = clear(text);
}
// Make sure input is valid.
if (typeof text !== 'string') {
return 'Input text should be a string!';
}
if (!text.trim()) {
return 'Please provide a valid text input';
}
// Spread string into characters and return them in an array.
const characters = [...(text.charAt(0).toLocaleUpperCase() + text.slice(1)).replace(/\s/g, '')];
return characters;
}
/**
* Validates if a string is a valid email address according to RFC 5322 standards.
*
* This function performs comprehensive validation including:
* - Basic email structure (local part + @ + domain)
* - Valid characters in local part and domain
* - Domain must contain at least one dot
* - No consecutive dots
* - No trailing dots or hyphens
* - Length limits (local part ≤ 64 chars, domain ≤ 255 chars)
* - Non-ASCII character rejection
*
* @param text - The string to validate as an email address
* @returns boolean indicating if the string is a valid email address
*
* @example
* ```typescript
* isEmail('user@example.com') // true
* isEmail('user.name@example.com') // true
* isEmail('user+tag@example.com') // true
* isEmail('plainaddress') // false
* isEmail('@example.com') // false
* isEmail('user@') // false
* ```
*/
function isEmail(text) {
if (!text || typeof text !== 'string') {
return false;
}
const email = text.trim();
// Reject non-ASCII characters
if (/[^\x20-\x7E]/.test(email)) {
return false;
}
const emailRegex = /^(?!.*\.{2})[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(email)) {
return false;
}
const [localPart, domain] = email.split('@');
if (!localPart || !domain)
return false;
if (localPart.length > 64 || domain.length > 255)
return false;
if (domain.startsWith('-') || domain.endsWith('-'))
return false;
if (domain.startsWith('.') || domain.endsWith('.'))
return false;
return true;
}
// Create digits in words
const numbers = 'zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen'.split(' ');
const tens = 'twenty thirty forty fifty sixty seventy eighty ninety'.split(' ');
/**
* Get any number below 100 million converted to words.
* @param number Integer input to turn into text.
* @returns A string of numbers converted to words.
* @example
* numbersToWords(12345); // 'twelve thousand three hundred and forty-five'
*/
function numbersToWords(number) {
// Check if the number is less than 100 million
if (number >= 100000000) {
throw new Error('Please enter a number under 100 million!');
}
// Check if the input is between 0-19
if (number < 20)
return numbers[number];
// Create a digit variable
const digit = number % 10;
// Check if the input is between 20-99
if (number < 100)
return tens[~~(number / 10) - 2] + (digit ? '-' + numbers[digit] : '');
// Check if the input is between 100 and 999
if (number < 1000)
return (numbers[~~(number / 100)] +
' hundred' +
(number % 100 == 0 ? '' : ' and ' + numbersToWords(number % 100)));
// Check if the input is between 1,000 and 999,999
if (number < 1000000) {
const thousands = ~~(number / 1000);
const remainder = number % 1000;
return (numbersToWords(thousands) +
' thousand' +
(remainder != 0 ? ' ' + numbersToWords(remainder) : ''));
}
// Handle millions (1,000,000 to 99,999,999)
const millions = ~~(number / 1000000);
const remainder = number % 1000000;
return (numbersToWords(millions) + ' million' + (remainder != 0 ? ' ' + numbersToWords(remainder) : ''));
}
export { Language, camelCase, clear, count, countSentences, countWords, detectLanguage, getTextStats, isEmail, kebabCase, numbersToWords, pascalCase, reverse, snakeCase, spread };
//# sourceMappingURL=textConvert.mjs.map