cspell-lib 10.2.0 → 10.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/Transform/IntlSegmentTextTransformer.d.ts +1 -0
- package/dist/lib/Transform/IntlSegmentTextTransformer.js +4 -3
- package/dist/lib/textValidation/isRandomString.js +4 -3
- package/dist/lib/textValidation/lineValidatorFactory.js +7 -2
- package/dist/lib/textValidation/traceWord.js +1 -1
- package/dist/lib/util/textRegex.js +1 -1
- package/dist/lib/util/wordSplitter/generateWordBreaks.d.ts +32 -0
- package/dist/lib/util/wordSplitter/generateWordBreaks.js +120 -0
- package/dist/lib/util/wordSplitter/index.d.ts +4 -0
- package/dist/lib/util/wordSplitter/index.js +3 -0
- package/dist/lib/util/{wordSplitter.d.ts → wordSplitter/wordSplitter.d.ts} +13 -22
- package/dist/lib/util/wordSplitter/wordSplitter.js +338 -0
- package/dist/lib/util/{wordSplitter.perf.js → wordSplitter/wordSplitter.perf.js} +2 -2
- package/package.json +20 -19
- package/dist/lib/util/wordSplitter.js +0 -331
- /package/dist/lib/util/{wordSplitter.perf.d.ts → wordSplitter/wordSplitter.perf.d.ts} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MappedText } from '@cspell/cspell-types';
|
|
2
2
|
import type { TextTransformer } from './Transformer.js';
|
|
3
|
+
export declare const softHyphen = "\u00AD";
|
|
3
4
|
export declare class IntlSegmentTextTransformer implements TextTransformer {
|
|
4
5
|
#private;
|
|
5
6
|
constructor(locale: string | string[]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeLocaleIntl } from '../Settings/LanguageSettings.js';
|
|
2
2
|
import { applyEditsToMappedText } from './TextMapEdit.js';
|
|
3
3
|
import { toMappedText } from './Transformer.js';
|
|
4
|
+
export const softHyphen = '\u00AD';
|
|
4
5
|
const regExSplitWordCharacters = /(?<=[\p{L}\p{M}])[\p{L}\p{M}]/uy;
|
|
5
6
|
export class IntlSegmentTextTransformer {
|
|
6
7
|
#locale;
|
|
@@ -23,11 +24,11 @@ function segmentText(mText, segmenter) {
|
|
|
23
24
|
let lastIndex = -1;
|
|
24
25
|
const reg = new RegExp(regExSplitWordCharacters);
|
|
25
26
|
const text = mText.text;
|
|
26
|
-
for (const { segment, index } of segmenter.segment(mText.text)) {
|
|
27
|
+
for (const { segment, index, isWordLike } of segmenter.segment(mText.text)) {
|
|
27
28
|
const endIndex = index + segment.length;
|
|
28
29
|
reg.lastIndex = index;
|
|
29
|
-
if (index === lastIndex && reg.test(text)) {
|
|
30
|
-
const edit = { text:
|
|
30
|
+
if (isWordLike && index === lastIndex && reg.test(text)) {
|
|
31
|
+
const edit = { text: softHyphen + segment, range: [index, endIndex] };
|
|
31
32
|
edits.push(edit);
|
|
32
33
|
}
|
|
33
34
|
lastIndex = endIndex;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { softHyphen } from '../util/wordSplitter/index.js';
|
|
1
2
|
const maxRadio = 0.5;
|
|
2
3
|
/**
|
|
3
4
|
* Try to detect if a string is a random string of characters or is it camel case / snake case words.
|
|
@@ -5,6 +6,9 @@ const maxRadio = 0.5;
|
|
|
5
6
|
* @returns true if the string is considered random;
|
|
6
7
|
*/
|
|
7
8
|
export function isRandomString(s, maxNoiseToLengthRatio = maxRadio) {
|
|
9
|
+
// Soft hyphen indicates a word break, so it's likely not a random string.
|
|
10
|
+
if (s.includes(softHyphen))
|
|
11
|
+
return false;
|
|
8
12
|
return scoreRandomString(s) >= maxNoiseToLengthRatio;
|
|
9
13
|
}
|
|
10
14
|
/**
|
|
@@ -28,9 +32,6 @@ function sumWordLengths(s) {
|
|
|
28
32
|
}
|
|
29
33
|
return total;
|
|
30
34
|
}
|
|
31
|
-
function _nonLetterRatio(s) {
|
|
32
|
-
return s.replaceAll(/\p{L}/gu, '').length / s.length;
|
|
33
|
-
}
|
|
34
35
|
export function categorizeString(s) {
|
|
35
36
|
const n = s
|
|
36
37
|
.replaceAll(/\d+/g, '0')
|
|
@@ -7,7 +7,7 @@ import { mapRangeBackToOriginalPos } from '../Transform/index.js';
|
|
|
7
7
|
import { autoResolve } from '../util/AutoResolve.js';
|
|
8
8
|
import { extractPossibleWordsFromTextOffset, extractText, extractWordsFromTextOffset, splitWordWithOffset, } from '../util/text.js';
|
|
9
9
|
import { regExpCamelCaseWordBreaksWithEnglishSuffix } from '../util/textRegex.js';
|
|
10
|
-
import { split } from '../util/wordSplitter.js';
|
|
10
|
+
import { split } from '../util/wordSplitter/index.js';
|
|
11
11
|
import { defaultMinWordLength } from './defaultConstants.js';
|
|
12
12
|
import { extractHexSequences, isRandomString } from './isRandomString.js';
|
|
13
13
|
import { isWordValidWithEscapeRetry } from './isWordValid.js';
|
|
@@ -327,7 +327,12 @@ export function lineValidatorFactory(sDict, options) {
|
|
|
327
327
|
const v = checkWord({ ...w, text: m[1], line: lineSegment.line });
|
|
328
328
|
return v.isFlagged || !v.isFound;
|
|
329
329
|
});
|
|
330
|
-
|
|
330
|
+
// Ignore 1 or 2 letter segments.
|
|
331
|
+
const minWordLen = Math.min(3, minWordLength);
|
|
332
|
+
const filtered = filterExcludedTextOffsets(nonMatching
|
|
333
|
+
.map((w) => ({ ...w, line: lineSegment.line }))
|
|
334
|
+
.map(annotateIsFlagged)
|
|
335
|
+
.filter((w) => w.isFlagged || w.text.length >= minWordLen), hexSequences);
|
|
331
336
|
if (filtered.length < mismatches.length) {
|
|
332
337
|
return filtered;
|
|
333
338
|
}
|
|
@@ -2,7 +2,7 @@ import { getSources } from '../Settings/index.js';
|
|
|
2
2
|
import { createCollection, getInlineConfigDictionaries, mapSpecialDictionaryNamesToSettings, } from '../SpellingDictionary/index.js';
|
|
3
3
|
import { toFileUrl } from '../util/url.js';
|
|
4
4
|
import { uniqueFn } from '../util/util.js';
|
|
5
|
-
import { split } from '../util/wordSplitter.js';
|
|
5
|
+
import { split } from '../util/wordSplitter/index.js';
|
|
6
6
|
export function traceWord(word, dictCollection, config) {
|
|
7
7
|
const opts = {
|
|
8
8
|
ignoreCase: config.ignoreCase ?? true,
|
|
@@ -7,7 +7,7 @@ export const regExpCamelCaseWordBreaks_NOT_USED = /(?<=\p{Ll}\p{M}?)(?=\p{Lu})|(
|
|
|
7
7
|
export const regExpAllPossibleWordBreaks_NOT_USED = /(?<=\p{Ll}\p{M}?)(?=\p{Lu})|(?<=\p{Lu}\p{M}?)(?=\p{Lu}\p{M}?\p{Ll})|(?<=\p{Lu}\p{M}?\p{Lu}\p{M}?)(?=\p{Ll})|(?<=\p{L}\p{M}?)(?=\P{L})|(?<=\P{L})(?=\p{L})/gu;
|
|
8
8
|
export const regExWords = /\p{L}\p{M}?(?:(?:\\?['’])?\p{L}\p{M}?)*/gu;
|
|
9
9
|
// Words can be made of letters, numbers, period, underscore, dash, plus, and single quote
|
|
10
|
-
export const regExWordsAndDigits = /[\p{L}\w'
|
|
10
|
+
export const regExWordsAndDigits = /[\p{L}\w'’`.+\u00AD-](?:(?:\\(?=[']))?[\p{L}\p{M}\w'’`.+\u00AD-])*/gu;
|
|
11
11
|
export const regExIgnoreCharacters = /[\p{sc=Hiragana}\p{sc=Han}\p{sc=Katakana}\u30A0-\u30FF\p{sc=Hangul}]/gu;
|
|
12
12
|
export const regExFirstUpper = /^\p{Lu}\p{M}?\p{Ll}+$/u;
|
|
13
13
|
export const regExAllUpper = /^(?:\p{Lu}\p{M}?)+$/u;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TextOffset } from '@cspell/cspell-types';
|
|
2
|
+
export declare const softHyphen = "\u00AD";
|
|
3
|
+
export interface LineSegment {
|
|
4
|
+
line: TextOffset;
|
|
5
|
+
/**
|
|
6
|
+
* The starting offset within line.text.
|
|
7
|
+
* The absolute offset can be obtained by adding line.offset.
|
|
8
|
+
*/
|
|
9
|
+
relStart: number;
|
|
10
|
+
/**
|
|
11
|
+
* The ending offset within line.text.
|
|
12
|
+
* The absolute offset can be obtained by adding line.offset.
|
|
13
|
+
*/
|
|
14
|
+
relEnd: number;
|
|
15
|
+
}
|
|
16
|
+
export type BreakPairs = readonly [number, number];
|
|
17
|
+
export interface PossibleWordBreak {
|
|
18
|
+
/** offset from the start of the string */
|
|
19
|
+
offset: number;
|
|
20
|
+
/**
|
|
21
|
+
* break pairs (start, end)
|
|
22
|
+
* (the characters between the start and end are removed)
|
|
23
|
+
* With a pure break, start === end.
|
|
24
|
+
*/
|
|
25
|
+
breaks: BreakPairs[];
|
|
26
|
+
}
|
|
27
|
+
export type SortedBreaks = PossibleWordBreak[];
|
|
28
|
+
export interface WordBreakOptions {
|
|
29
|
+
optionalWordBreakCharacters?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function generateWordBreaks(line: LineSegment, options: WordBreakOptions): SortedBreaks;
|
|
32
|
+
//# sourceMappingURL=generateWordBreaks.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { escapeRegEx } from '../regexHelper.js';
|
|
2
|
+
import { regExDanglingQuote, regExEscapeCharacters, regExPossibleWordBreaks, regExSplitWords, regExSplitWords2, regExTrailingEndings, } from '../textRegex.js';
|
|
3
|
+
export const softHyphen = '\u00AD';
|
|
4
|
+
const ignoreBreak = Object.freeze([]);
|
|
5
|
+
export function generateWordBreaks(line, options) {
|
|
6
|
+
const camelBreaks = genWordBreakCamel(line);
|
|
7
|
+
const symbolBreaks = genSymbolBreaks(line);
|
|
8
|
+
const optionalBreaks = genOptionalWordBreaks(line, options.optionalWordBreakCharacters);
|
|
9
|
+
return mergeSortedBreaks(...camelBreaks, ...symbolBreaks, ...optionalBreaks);
|
|
10
|
+
}
|
|
11
|
+
function offsetRegEx(reg, offset) {
|
|
12
|
+
const r = new RegExp(reg);
|
|
13
|
+
r.lastIndex = offset;
|
|
14
|
+
return r;
|
|
15
|
+
}
|
|
16
|
+
function genWordBreakCamel(line) {
|
|
17
|
+
const breaksCamel1 = [];
|
|
18
|
+
const text = line.line.text.slice(0, line.relEnd);
|
|
19
|
+
// lower,Upper: camelCase -> camel|Case
|
|
20
|
+
for (const m of text.matchAll(offsetRegEx(regExSplitWords, line.relStart))) {
|
|
21
|
+
if (m.index === undefined)
|
|
22
|
+
break;
|
|
23
|
+
const i = m.index + m[1].length;
|
|
24
|
+
breaksCamel1.push({
|
|
25
|
+
offset: m.index,
|
|
26
|
+
breaks: [[i, i], ignoreBreak],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const breaksCamel2 = [];
|
|
30
|
+
// cspell:ignore ERRORC
|
|
31
|
+
// Upper,Upper,lower: ERRORCodes -> ERROR|Codes, ERRORC|odes
|
|
32
|
+
for (const m of text.matchAll(offsetRegEx(regExSplitWords2, line.relStart))) {
|
|
33
|
+
if (m.index === undefined)
|
|
34
|
+
break;
|
|
35
|
+
const i = m.index + m[1].length;
|
|
36
|
+
const j = i + m[3].length;
|
|
37
|
+
breaksCamel2.push({
|
|
38
|
+
offset: m.index,
|
|
39
|
+
breaks: [[i, i], [j, j], ignoreBreak],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return [breaksCamel1, breaksCamel2];
|
|
43
|
+
}
|
|
44
|
+
function calcBreaksForSubString(line, subStr, calcBreak) {
|
|
45
|
+
const sb = [];
|
|
46
|
+
const text = line.line.text;
|
|
47
|
+
const inc = subStr.length;
|
|
48
|
+
for (let pos = text.indexOf(subStr, line.relStart); pos >= 0 && pos < line.relEnd; pos = text.indexOf(subStr, pos + inc)) {
|
|
49
|
+
const b = calcBreak(pos, pos + inc);
|
|
50
|
+
if (b) {
|
|
51
|
+
sb.push(b);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return sb;
|
|
55
|
+
}
|
|
56
|
+
function calcBreaksForRegEx(line, reg, calcBreak) {
|
|
57
|
+
const sb = [];
|
|
58
|
+
const text = line.line.text.slice(0, line.relEnd);
|
|
59
|
+
for (const m of text.matchAll(offsetRegEx(reg, line.relStart))) {
|
|
60
|
+
const b = calcBreak(m);
|
|
61
|
+
if (b) {
|
|
62
|
+
sb.push(b);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return sb;
|
|
66
|
+
}
|
|
67
|
+
function calcOptionalWordBreak(i, j) {
|
|
68
|
+
return {
|
|
69
|
+
offset: i,
|
|
70
|
+
breaks: [
|
|
71
|
+
[i, j], // Remove the characters
|
|
72
|
+
ignoreBreak,
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function genOptionalWordBreaks(line, optionalBreakCharacters) {
|
|
77
|
+
function calcBreaks(m) {
|
|
78
|
+
const i = m.index;
|
|
79
|
+
if (i === undefined)
|
|
80
|
+
return;
|
|
81
|
+
const j = i + m[0].length;
|
|
82
|
+
return calcOptionalWordBreak(i, j);
|
|
83
|
+
}
|
|
84
|
+
const breaks = [
|
|
85
|
+
calcBreaksForSubString(line, softHyphen, calcOptionalWordBreak),
|
|
86
|
+
calcBreaksForRegEx(line, regExDanglingQuote, calcBreaks),
|
|
87
|
+
calcBreaksForRegEx(line, regExTrailingEndings, calcBreaks),
|
|
88
|
+
];
|
|
89
|
+
if (optionalBreakCharacters) {
|
|
90
|
+
const regex = new RegExp(`[${escapeRegEx(optionalBreakCharacters)}]`, 'gu');
|
|
91
|
+
breaks.push(calcBreaksForRegEx(line, regex, calcBreaks));
|
|
92
|
+
}
|
|
93
|
+
return breaks;
|
|
94
|
+
}
|
|
95
|
+
function genSymbolBreaks(line) {
|
|
96
|
+
function calcBreaks(m) {
|
|
97
|
+
const i = m.index;
|
|
98
|
+
if (i === undefined)
|
|
99
|
+
return;
|
|
100
|
+
const j = i + m[0].length;
|
|
101
|
+
return {
|
|
102
|
+
offset: i,
|
|
103
|
+
breaks: [
|
|
104
|
+
[i, j], // Remove the characters
|
|
105
|
+
[i, i], // keep characters with word to right
|
|
106
|
+
[j, j], // keep characters with word to left
|
|
107
|
+
ignoreBreak,
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return [
|
|
112
|
+
calcBreaksForRegEx(line, regExPossibleWordBreaks, calcBreaks),
|
|
113
|
+
calcBreaksForRegEx(line, /\d+/g, calcBreaks),
|
|
114
|
+
calcBreaksForRegEx(line, regExEscapeCharacters, calcBreaks),
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
function mergeSortedBreaks(...maps) {
|
|
118
|
+
return maps.flat().sort((a, b) => a.offset - b.offset);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=generateWordBreaks.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TextOffset } from '@cspell/cspell-types';
|
|
2
|
+
import type { WordBreakOptions } from './generateWordBreaks.js';
|
|
2
3
|
export type IsValidWordFn = (word: TextOffset) => boolean;
|
|
3
4
|
export interface SplitResult {
|
|
4
5
|
/** Original line passed to the split function */
|
|
@@ -12,36 +13,26 @@ export interface SplitResult {
|
|
|
12
13
|
/** the offset at which the split stopped */
|
|
13
14
|
endOffset: number;
|
|
14
15
|
}
|
|
15
|
-
export interface
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
relEnd: number;
|
|
16
|
+
export interface TextOffsetWithOptionalValid extends TextOffset {
|
|
17
|
+
length: number;
|
|
18
|
+
isFound?: boolean;
|
|
19
19
|
}
|
|
20
|
-
export interface TextOffsetWithValid extends
|
|
20
|
+
export interface TextOffsetWithValid extends TextOffsetWithOptionalValid {
|
|
21
21
|
isFound: boolean;
|
|
22
22
|
}
|
|
23
23
|
export interface SplitOptions extends WordBreakOptions {
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
*
|
|
27
|
+
* @param line - the line of text
|
|
28
|
+
* @param offset - absolute offset, comparable to line.offset.
|
|
29
|
+
* @param isValidWord - predicate function to test if a word is valid.
|
|
30
|
+
* @param options - SplitOptions
|
|
31
|
+
* @returns SplitResult
|
|
32
|
+
*/
|
|
25
33
|
export declare function split(line: TextOffset, offset: number, isValidWord: IsValidWordFn, options?: SplitOptions): SplitResult;
|
|
26
34
|
declare function findNextWordText({ text, offset }: TextOffset): TextOffset;
|
|
27
|
-
type BreakPairs = readonly [number, number];
|
|
28
|
-
interface PossibleWordBreak {
|
|
29
|
-
/** offset from the start of the string */
|
|
30
|
-
offset: number;
|
|
31
|
-
/**
|
|
32
|
-
* break pairs (start, end)
|
|
33
|
-
* (the characters between the start and end are removed)
|
|
34
|
-
* With a pure break, start === end.
|
|
35
|
-
*/
|
|
36
|
-
breaks: BreakPairs[];
|
|
37
|
-
}
|
|
38
|
-
export type SortedBreaks = PossibleWordBreak[];
|
|
39
|
-
interface WordBreakOptions {
|
|
40
|
-
optionalWordBreakCharacters?: string;
|
|
41
|
-
}
|
|
42
|
-
declare function generateWordBreaks(line: LineSegment, options: WordBreakOptions): SortedBreaks;
|
|
43
35
|
export declare const __testing__: {
|
|
44
|
-
generateWordBreaks: typeof generateWordBreaks;
|
|
45
36
|
findNextWordText: typeof findNextWordText;
|
|
46
37
|
};
|
|
47
38
|
export {};
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { PairingHeap } from '../PairingHeap.js';
|
|
2
|
+
import { regExFirstUpper, regExNumericLiteral, regExWordsAndDigits } from '../textRegex.js';
|
|
3
|
+
import { generateWordBreaks, softHyphen } from './generateWordBreaks.js';
|
|
4
|
+
const ignoreBreak = Object.freeze([]);
|
|
5
|
+
const maxSkippedBreaks = 16; // Maximum number of breaks that can be skipped during word splitting.
|
|
6
|
+
const maxAttempts = 64_000; // Maximum number of attempts to split words.
|
|
7
|
+
// Quote/backtick characters that `regExWordsAndDigits` treats as word characters, so a quoted
|
|
8
|
+
// or templated string literal in source code gets scanned with its delimiters attached.
|
|
9
|
+
const regExLeadingTrimCharacters = /^['’`\\-]+/;
|
|
10
|
+
const regExTrailingTrimCharacters = /['’`\\-]+$/;
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* @param line - the line of text
|
|
14
|
+
* @param offset - absolute offset, comparable to line.offset.
|
|
15
|
+
* @param isValidWord - predicate function to test if a word is valid.
|
|
16
|
+
* @param options - SplitOptions
|
|
17
|
+
* @returns SplitResult
|
|
18
|
+
*/
|
|
19
|
+
export function split(line, offset, isValidWord, options = {}) {
|
|
20
|
+
const relWordToSplit = findNextWordText({ text: line.text, offset: offset - line.offset });
|
|
21
|
+
const hasSoftHyphen = relWordToSplit.text.includes(softHyphen);
|
|
22
|
+
const lineOffset = line.offset;
|
|
23
|
+
const requested = new Map();
|
|
24
|
+
const regExpIgnoreSegment = /^[-.+\d_eE'`\\\s\u00AD]+$/;
|
|
25
|
+
if (!relWordToSplit.text) {
|
|
26
|
+
const text = rebaseTextOffsetToAbsolute(relWordToSplit);
|
|
27
|
+
return {
|
|
28
|
+
line,
|
|
29
|
+
offset,
|
|
30
|
+
text: text,
|
|
31
|
+
words: [],
|
|
32
|
+
endOffset: text.offset + (text.length ?? text.text.length),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const lineSegment = {
|
|
36
|
+
line,
|
|
37
|
+
relStart: relWordToSplit.offset,
|
|
38
|
+
relEnd: relWordToSplit.offset + relWordToSplit.text.length,
|
|
39
|
+
};
|
|
40
|
+
const possibleBreaks = generateWordBreaks(lineSegment, options);
|
|
41
|
+
if (!possibleBreaks.length) {
|
|
42
|
+
const text = rebaseTextOffsetToAbsolute(relWordToSplit);
|
|
43
|
+
return {
|
|
44
|
+
line,
|
|
45
|
+
offset,
|
|
46
|
+
text: text,
|
|
47
|
+
words: [{ ...text, length: text.length || text.text.length, isFound: isValidWord(text) }],
|
|
48
|
+
endOffset: text.offset + (text.length ?? text.text.length),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function rebaseTextOffsetToAbsolute(relText) {
|
|
52
|
+
relText.offset += lineOffset;
|
|
53
|
+
return relText;
|
|
54
|
+
}
|
|
55
|
+
function rebaseTextOffsetToRelative(relText) {
|
|
56
|
+
relText.offset -= lineOffset;
|
|
57
|
+
return relText;
|
|
58
|
+
}
|
|
59
|
+
function createTextOffsetWithValid(start, end, isFound) {
|
|
60
|
+
const word = {
|
|
61
|
+
offset: start,
|
|
62
|
+
length: end - start,
|
|
63
|
+
text: line.text.slice(start, end),
|
|
64
|
+
isFound,
|
|
65
|
+
};
|
|
66
|
+
return word;
|
|
67
|
+
}
|
|
68
|
+
function calcKey(start, end) {
|
|
69
|
+
const i = start;
|
|
70
|
+
const j = end - start;
|
|
71
|
+
const v = i + (j << 20);
|
|
72
|
+
return i < 1 << 20 && j < 1 << 11 ? v : -1;
|
|
73
|
+
}
|
|
74
|
+
function checkTextRange(start, end) {
|
|
75
|
+
const v = calcKey(start, end);
|
|
76
|
+
const f = v >= 0 ? requested.get(v) : undefined;
|
|
77
|
+
if (f)
|
|
78
|
+
return f;
|
|
79
|
+
const validated = createTextOffsetWithValid(start, end, false);
|
|
80
|
+
if (regExpIgnoreSegment.test(validated.text)) {
|
|
81
|
+
validated.isFound = true;
|
|
82
|
+
if (v >= 0) {
|
|
83
|
+
requested.set(v, validated);
|
|
84
|
+
}
|
|
85
|
+
return validated;
|
|
86
|
+
}
|
|
87
|
+
rebaseTextOffsetToAbsolute(validated);
|
|
88
|
+
validated.isFound = isValidWord(validated);
|
|
89
|
+
if (hasSoftHyphen && !validated.isFound && validated.text.includes(softHyphen)) {
|
|
90
|
+
removeSoftHyphen(validated);
|
|
91
|
+
validated.isFound = isValidWord(validated);
|
|
92
|
+
}
|
|
93
|
+
rebaseTextOffsetToRelative(validated);
|
|
94
|
+
if (v >= 0) {
|
|
95
|
+
requested.set(v, validated);
|
|
96
|
+
}
|
|
97
|
+
return validated;
|
|
98
|
+
}
|
|
99
|
+
// Add a dummy break at the end to avoid needing to check for last break.
|
|
100
|
+
possibleBreaks.push({
|
|
101
|
+
offset: lineSegment.relEnd,
|
|
102
|
+
breaks: [ignoreBreak],
|
|
103
|
+
});
|
|
104
|
+
const result = {
|
|
105
|
+
line,
|
|
106
|
+
offset,
|
|
107
|
+
text: rebaseTextOffsetToAbsolute(relWordToSplit),
|
|
108
|
+
words: splitIntoWords(lineSegment, possibleBreaks, checkTextRange).map(rebaseTextOffsetToAbsolute),
|
|
109
|
+
endOffset: lineOffset + lineSegment.relEnd,
|
|
110
|
+
};
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
function removeSoftHyphen(word) {
|
|
114
|
+
word.text = word.text.replaceAll(softHyphen, '');
|
|
115
|
+
return word;
|
|
116
|
+
}
|
|
117
|
+
function findNextWordText({ text, offset }) {
|
|
118
|
+
const reg = new RegExp(regExWordsAndDigits);
|
|
119
|
+
reg.lastIndex = offset;
|
|
120
|
+
const m = reg.exec(text);
|
|
121
|
+
if (!m) {
|
|
122
|
+
// No more words found, return an empty text with offset at the end of the string within the document.
|
|
123
|
+
return { text: '', offset: offset + text.length };
|
|
124
|
+
}
|
|
125
|
+
// Skip numeric literals.
|
|
126
|
+
if (regExNumericLiteral.test(m[0])) {
|
|
127
|
+
return findNextWordText({ text, offset: offset + m[0].length });
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
text: m[0],
|
|
131
|
+
offset: m.index,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function splitIntoWords(lineSeg, breaks, checkTextRange) {
|
|
135
|
+
const maxIndex = lineSeg.relEnd;
|
|
136
|
+
/**
|
|
137
|
+
* Map of known paths by their starting index in the text.
|
|
138
|
+
* This helps in quickly finding previously computed paths to avoid redundant calculations.
|
|
139
|
+
*/
|
|
140
|
+
const knownPathsByIndex = new Map();
|
|
141
|
+
const terminalNode = { n: undefined, i: maxIndex, j: maxIndex, c: 0, nc: 0, text: undefined };
|
|
142
|
+
knownPathsByIndex.set(maxIndex, terminalNode);
|
|
143
|
+
// Seed the search with the whole word treated as a single candidate. This guarantees a
|
|
144
|
+
// correct fallback result (e.g. a whole word matched via ignoreWords/the dictionary, or
|
|
145
|
+
// reporting the word as a single misspelling) even when the segmented search below never
|
|
146
|
+
// finds a strictly cheaper split.
|
|
147
|
+
let wholeWord = checkTextRange(lineSeg.relStart, maxIndex);
|
|
148
|
+
if (!wholeWord.isFound) {
|
|
149
|
+
// `regExWordsAndDigits` treats quote/backtick characters as word characters so that
|
|
150
|
+
// things like `Tom's` or `n'cpp` are scanned as a single word; but it also means a
|
|
151
|
+
// quoted/templated string literal in source code (e.g. `'ignoredWord'`) is scanned
|
|
152
|
+
// including its delimiters. Retry against the span with that framing peeled off so a
|
|
153
|
+
// whole word that is otherwise valid/ignored is still recognized as such.
|
|
154
|
+
const raw = lineSeg.line.text.slice(lineSeg.relStart, maxIndex);
|
|
155
|
+
const lead = raw.match(regExLeadingTrimCharacters)?.[0].length ?? 0;
|
|
156
|
+
const tail = raw.slice(lead).match(regExTrailingTrimCharacters)?.[0].length ?? 0;
|
|
157
|
+
if ((lead || tail) && lead + tail < raw.length) {
|
|
158
|
+
wholeWord = checkTextRange(lineSeg.relStart + lead, maxIndex - tail);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const wholeWordCost = wholeWord.isFound ? 0 : wholeWord.text.length;
|
|
162
|
+
const wholeWordNode = {
|
|
163
|
+
n: terminalNode,
|
|
164
|
+
i: lineSeg.relStart,
|
|
165
|
+
j: maxIndex,
|
|
166
|
+
c: wholeWordCost,
|
|
167
|
+
nc: wholeWordCost,
|
|
168
|
+
text: wholeWord,
|
|
169
|
+
};
|
|
170
|
+
knownPathsByIndex.set(lineSeg.relStart, wholeWordNode);
|
|
171
|
+
function findNearestBreakIndex(offset, bi) {
|
|
172
|
+
while (bi < breaks.length && breaks[bi].offset < offset) {
|
|
173
|
+
bi += 1;
|
|
174
|
+
}
|
|
175
|
+
return bi < breaks.length ? bi : undefined;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Create a set of possible candidate to consider
|
|
179
|
+
* @param p - prev candidate that lead to this one
|
|
180
|
+
* @param i - offset within the string
|
|
181
|
+
* @param bi - current index into the set of breaks
|
|
182
|
+
* @param currentCost - current cost accrued
|
|
183
|
+
*/
|
|
184
|
+
function makeCandidates(p, i, bi, bs, currentCost) {
|
|
185
|
+
const len = maxIndex;
|
|
186
|
+
const nBi = findNearestBreakIndex(i, bi);
|
|
187
|
+
const nBs = findNearestBreakIndex(i, bs);
|
|
188
|
+
if (nBi === undefined || nBs === undefined) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
bi = nBi;
|
|
192
|
+
bs = nBs;
|
|
193
|
+
if (bi - bs > maxSkippedBreaks) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
const br = breaks[bi];
|
|
197
|
+
function calcBreakCost(bp) {
|
|
198
|
+
if (bp.length < 2) {
|
|
199
|
+
// We are skipping this break pair.
|
|
200
|
+
return { p, i, j: len, bi, bs, bp, c: currentCost, ec: currentCost + len - i, text: undefined };
|
|
201
|
+
}
|
|
202
|
+
const startOfBreak = bp[0];
|
|
203
|
+
const endOfBreak = bp[1];
|
|
204
|
+
// cost from the end of the segment to the end of the text
|
|
205
|
+
// const costToEnd = knownPathsByIndex.get(endOfBreak)?.c ?? len - endOfBreak;
|
|
206
|
+
const costToEnd = len - endOfBreak;
|
|
207
|
+
// Guess the cost of the word.
|
|
208
|
+
const costOfWord = (startOfBreak - i) * 0.5;
|
|
209
|
+
const d = costOfWord + costToEnd;
|
|
210
|
+
const ec = currentCost + d;
|
|
211
|
+
return { p, i, j: endOfBreak, bi, bs, bp, c: currentCost, ec, text: undefined };
|
|
212
|
+
}
|
|
213
|
+
return br.breaks.map(calcBreakCost);
|
|
214
|
+
}
|
|
215
|
+
function compare(a, b) {
|
|
216
|
+
return a.bi - a.bs - (b.bi - b.bs) || a.ec - b.ec || b.i - a.i || a.j - b.j;
|
|
217
|
+
}
|
|
218
|
+
function pathToWords(node) {
|
|
219
|
+
const results = [];
|
|
220
|
+
for (let p = node; p; p = p.n) {
|
|
221
|
+
if (p.text) {
|
|
222
|
+
results.push(p.text);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return results;
|
|
226
|
+
}
|
|
227
|
+
function addToKnownPaths(candidate) {
|
|
228
|
+
let path = knownPathsByIndex.get(candidate.j);
|
|
229
|
+
for (let can = candidate; can !== undefined; can = can.p) {
|
|
230
|
+
const t = can.text;
|
|
231
|
+
const i = can.i;
|
|
232
|
+
const j = can.j;
|
|
233
|
+
const currentNode = knownPathsByIndex.get(i);
|
|
234
|
+
const exitingPath = knownPathsByIndex.get(j);
|
|
235
|
+
// If a better-or-equal path is already known, reuse it and keep propagating
|
|
236
|
+
// the (possibly cheaper) suffix back toward the start instead of abandoning it.
|
|
237
|
+
if (exitingPath && path && exitingPath.c <= path.c) {
|
|
238
|
+
path = exitingPath;
|
|
239
|
+
}
|
|
240
|
+
const nc = !t || t.isFound ? 0 : t.length;
|
|
241
|
+
const cost = nc + (path?.c || 0);
|
|
242
|
+
const node = { n: path, i, j, c: cost, nc, text: t };
|
|
243
|
+
if (!currentNode || currentNode.c > cost) {
|
|
244
|
+
knownPathsByIndex.set(i, node);
|
|
245
|
+
}
|
|
246
|
+
path = node;
|
|
247
|
+
}
|
|
248
|
+
return path;
|
|
249
|
+
}
|
|
250
|
+
// A candidate that only ties the whole-word fallback is still preferred over it, since it
|
|
251
|
+
// represents a more granular split; ties between two genuine split candidates keep whichever
|
|
252
|
+
// was found first. Allow the search to explore up to (and including) a tying cost by giving
|
|
253
|
+
// maxCost one extra unit of slack above the seeded whole-word cost.
|
|
254
|
+
function preferCandidate(candidate, current) {
|
|
255
|
+
if (candidate.c < current.c)
|
|
256
|
+
return true;
|
|
257
|
+
if (candidate.c > current.c)
|
|
258
|
+
return false;
|
|
259
|
+
return current === wholeWordNode && candidate !== wholeWordNode;
|
|
260
|
+
}
|
|
261
|
+
let maxCost = wholeWordCost + 1;
|
|
262
|
+
const candidates = new PairingHeap(compare);
|
|
263
|
+
candidates.append(makeCandidates(undefined, lineSeg.relStart, 0, 0, 0));
|
|
264
|
+
let attempts = 0;
|
|
265
|
+
let bestPath = wholeWordNode;
|
|
266
|
+
while (maxCost && candidates.length && attempts++ < maxAttempts) {
|
|
267
|
+
/** Best Candidate Index */
|
|
268
|
+
const best = candidates.dequeue();
|
|
269
|
+
if (!best || best.c >= maxCost) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
// Does it have a split?
|
|
273
|
+
if (best.bp.length) {
|
|
274
|
+
// yes
|
|
275
|
+
const i = best.bp[0];
|
|
276
|
+
const j = best.bp[1];
|
|
277
|
+
const parentPath = knownPathsByIndex.get(j);
|
|
278
|
+
const parentCost = parentPath?.c || 0;
|
|
279
|
+
if (parentCost + best.c >= maxCost) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const t = i > best.i ? checkTextRange(best.i, i) : undefined;
|
|
283
|
+
const cost = !t || t.isFound ? 0 : t.length;
|
|
284
|
+
const mc = maxIndex - j;
|
|
285
|
+
best.c += cost;
|
|
286
|
+
best.ec = best.c + mc;
|
|
287
|
+
best.text = t;
|
|
288
|
+
if (best.c >= maxCost) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (parentPath) {
|
|
292
|
+
// We found a known apply to candidate
|
|
293
|
+
const f = addToKnownPaths(best);
|
|
294
|
+
bestPath = f && preferCandidate(f, bestPath) ? f : bestPath;
|
|
295
|
+
}
|
|
296
|
+
const c = makeCandidates(t ? best : best.p, j, best.bi + 1, best.bs, best.c);
|
|
297
|
+
candidates.append(c);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// It is a pass through
|
|
301
|
+
const c = makeCandidates(best.p, best.i, best.bi + 1, best.bs, best.c);
|
|
302
|
+
candidates.append(c);
|
|
303
|
+
if (!c.length) {
|
|
304
|
+
const j = maxIndex;
|
|
305
|
+
const t = j > best.i ? checkTextRange(best.i, j) : undefined;
|
|
306
|
+
const cost = !t || t.isFound ? 0 : t.length;
|
|
307
|
+
best.c += cost;
|
|
308
|
+
best.ec = best.c;
|
|
309
|
+
best.text = t;
|
|
310
|
+
if (best.c >= maxCost) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const segText = t || best.p?.text || checkTextRange(best.i, best.i);
|
|
314
|
+
const can = t ? { ...best, text: segText } : { ...best, ...best.p, text: segText };
|
|
315
|
+
can.j = j;
|
|
316
|
+
const f = addToKnownPaths(can);
|
|
317
|
+
bestPath = f && preferCandidate(f, bestPath) ? f : bestPath;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// While the whole-word fallback is still the best known answer, keep one unit of
|
|
321
|
+
// slack above its cost so a genuine split that only ties it can still be explored
|
|
322
|
+
// to completion and preferred by preferCandidate. Once a real split has taken over,
|
|
323
|
+
// tighten exactly like before.
|
|
324
|
+
const slack = bestPath === wholeWordNode ? 1 : 0;
|
|
325
|
+
if (bestPath.c + slack < maxCost) {
|
|
326
|
+
maxCost = bestPath.c + slack;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const result = pathToWords(bestPath);
|
|
330
|
+
if (result.length === 2 && result.every((r) => !r.isFound) && regExFirstUpper.test(wholeWord.text)) {
|
|
331
|
+
return [wholeWord];
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
export const __testing__ = {
|
|
336
|
+
findNextWordText,
|
|
337
|
+
};
|
|
338
|
+
//# sourceMappingURL=wordSplitter.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { extname } from 'node:path/posix';
|
|
3
3
|
import { suite } from 'perf-insight';
|
|
4
|
-
import { extractPossibleWordsFromTextOffset, extractWordsFromCode, extractWordsFromText, matchStringToTextOffset, splitCamelCaseWordWithOffset, textOffset, } from '
|
|
5
|
-
import { regExWordsAndDigits } from '
|
|
4
|
+
import { extractPossibleWordsFromTextOffset, extractWordsFromCode, extractWordsFromText, matchStringToTextOffset, splitCamelCaseWordWithOffset, textOffset, } from '../text.js';
|
|
5
|
+
import { regExWordsAndDigits } from '../textRegex.js';
|
|
6
6
|
const regExpWord = /\b[\w\p{L}\p{M}]+\b/gu;
|
|
7
7
|
suite('wordSplitter', async (test) => {
|
|
8
8
|
const lines = await sampleLines();
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public",
|
|
5
5
|
"provenance": true
|
|
6
6
|
},
|
|
7
|
-
"version": "10.2.
|
|
7
|
+
"version": "10.2.2",
|
|
8
8
|
"description": "A library of useful functions used across various cspell tools.",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"sideEffects": false,
|
|
@@ -75,22 +75,22 @@
|
|
|
75
75
|
},
|
|
76
76
|
"homepage": "https://github.com/streetsidesoftware/cspell/tree/main/packages/cspell-lib#readme",
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@cspell/cspell-bundled-dicts": "10.2.
|
|
79
|
-
"@cspell/cspell-performance-monitor": "10.2.
|
|
80
|
-
"@cspell/cspell-pipe": "10.2.
|
|
81
|
-
"@cspell/cspell-resolver": "10.2.
|
|
82
|
-
"@cspell/cspell-types": "10.2.
|
|
83
|
-
"@cspell/dynamic-import": "10.2.
|
|
84
|
-
"@cspell/filetypes": "10.2.
|
|
85
|
-
"@cspell/rpc": "10.2.
|
|
86
|
-
"@cspell/strong-weak-map": "10.2.
|
|
87
|
-
"@cspell/url": "10.2.
|
|
88
|
-
"cspell-config-lib": "10.2.
|
|
89
|
-
"cspell-dictionary": "10.2.
|
|
90
|
-
"cspell-glob": "10.2.
|
|
91
|
-
"cspell-grammar": "10.2.
|
|
92
|
-
"cspell-io": "10.2.
|
|
93
|
-
"cspell-trie-lib": "10.2.
|
|
78
|
+
"@cspell/cspell-bundled-dicts": "10.2.2",
|
|
79
|
+
"@cspell/cspell-performance-monitor": "10.2.2",
|
|
80
|
+
"@cspell/cspell-pipe": "10.2.2",
|
|
81
|
+
"@cspell/cspell-resolver": "10.2.2",
|
|
82
|
+
"@cspell/cspell-types": "10.2.2",
|
|
83
|
+
"@cspell/dynamic-import": "10.2.2",
|
|
84
|
+
"@cspell/filetypes": "10.2.2",
|
|
85
|
+
"@cspell/rpc": "10.2.2",
|
|
86
|
+
"@cspell/strong-weak-map": "10.2.2",
|
|
87
|
+
"@cspell/url": "10.2.2",
|
|
88
|
+
"cspell-config-lib": "10.2.2",
|
|
89
|
+
"cspell-dictionary": "10.2.2",
|
|
90
|
+
"cspell-glob": "10.2.2",
|
|
91
|
+
"cspell-grammar": "10.2.2",
|
|
92
|
+
"cspell-io": "10.2.2",
|
|
93
|
+
"cspell-trie-lib": "10.2.2",
|
|
94
94
|
"env-paths": "^4.0.0",
|
|
95
95
|
"gensequence": "^8.0.8",
|
|
96
96
|
"import-fresh": "^4.0.0",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"node": ">=22.18.0"
|
|
104
104
|
},
|
|
105
105
|
"devDependencies": {
|
|
106
|
-
"@cspell/cspell-tools": "10.2.
|
|
106
|
+
"@cspell/cspell-tools": "10.2.2",
|
|
107
107
|
"@cspell/dict-cpp": "^7.1.0",
|
|
108
108
|
"@cspell/dict-csharp": "^4.0.8",
|
|
109
109
|
"@cspell/dict-css": "^4.1.2",
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"@cspell/dict-html": "^4.0.16",
|
|
114
114
|
"@cspell/dict-nl-nl": "^2.4.2",
|
|
115
115
|
"@cspell/dict-python": "^4.5.0",
|
|
116
|
+
"@cspell/dict-th-th": "^1.0.0",
|
|
116
117
|
"@cspell/dictionary-bundler-plugin": "",
|
|
117
118
|
"@types/configstore": "^6.0.2",
|
|
118
119
|
"comment-json": "^5.0.0",
|
|
@@ -122,5 +123,5 @@
|
|
|
122
123
|
"lorem-ipsum": "^3.0.0",
|
|
123
124
|
"perf-insight": "^2.0.1"
|
|
124
125
|
},
|
|
125
|
-
"gitHead": "
|
|
126
|
+
"gitHead": "22fae7ac63d4330c52fe97eebee743e3541f2e36"
|
|
126
127
|
}
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
import { PairingHeap } from './PairingHeap.js';
|
|
2
|
-
import { escapeRegEx } from './regexHelper.js';
|
|
3
|
-
import { regExDanglingQuote, regExEscapeCharacters, regExNumericLiteral, regExPossibleWordBreaks, regExSplitWords, regExSplitWords2, regExTrailingEndings, regExWordsAndDigits, } from './textRegex.js';
|
|
4
|
-
const ignoreBreak = Object.freeze([]);
|
|
5
|
-
export function split(line, offset, isValidWord, options = {}) {
|
|
6
|
-
const relWordToSplit = findNextWordText({ text: line.text, offset: offset - line.offset });
|
|
7
|
-
const lineOffset = line.offset;
|
|
8
|
-
const requested = new Map();
|
|
9
|
-
const regExpIgnoreSegment = /^[-.+\d_eE'`\\\s]+$/;
|
|
10
|
-
if (!relWordToSplit.text) {
|
|
11
|
-
const text = rebaseTextOffset(relWordToSplit);
|
|
12
|
-
return {
|
|
13
|
-
line,
|
|
14
|
-
offset,
|
|
15
|
-
text: text,
|
|
16
|
-
words: [],
|
|
17
|
-
endOffset: text.offset + text.text.length,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
const lineSegment = {
|
|
21
|
-
line,
|
|
22
|
-
relStart: relWordToSplit.offset,
|
|
23
|
-
relEnd: relWordToSplit.offset + relWordToSplit.text.length,
|
|
24
|
-
};
|
|
25
|
-
const possibleBreaks = generateWordBreaks(lineSegment, options);
|
|
26
|
-
if (!possibleBreaks.length) {
|
|
27
|
-
const text = rebaseTextOffset(relWordToSplit);
|
|
28
|
-
return {
|
|
29
|
-
line,
|
|
30
|
-
offset,
|
|
31
|
-
text: text,
|
|
32
|
-
words: [{ ...text, isFound: isValidWord(text) }],
|
|
33
|
-
endOffset: text.offset + text.text.length,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function rebaseTextOffset(relText) {
|
|
37
|
-
return {
|
|
38
|
-
...relText,
|
|
39
|
-
offset: relText.offset + lineOffset,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function has(word) {
|
|
43
|
-
if (regExpIgnoreSegment.test(word.text)) {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
const i = word.offset;
|
|
47
|
-
const j = word.text.length;
|
|
48
|
-
let v = i + (j << 20);
|
|
49
|
-
if (i < 1 << 20 && j < 1 << 11) {
|
|
50
|
-
const b = requested.get(v);
|
|
51
|
-
if (b !== undefined)
|
|
52
|
-
return b;
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
v = -1;
|
|
56
|
-
}
|
|
57
|
-
const r = isValidWord(rebaseTextOffset(word));
|
|
58
|
-
if (v >= 0) {
|
|
59
|
-
requested.set(v, r);
|
|
60
|
-
}
|
|
61
|
-
return r;
|
|
62
|
-
}
|
|
63
|
-
// Add a dummy break at the end to avoid needing to check for last break.
|
|
64
|
-
possibleBreaks.push({
|
|
65
|
-
offset: lineSegment.relEnd,
|
|
66
|
-
breaks: [ignoreBreak],
|
|
67
|
-
});
|
|
68
|
-
const result = {
|
|
69
|
-
line,
|
|
70
|
-
offset,
|
|
71
|
-
text: rebaseTextOffset(relWordToSplit),
|
|
72
|
-
words: splitIntoWords(lineSegment, possibleBreaks, has).map(rebaseTextOffset),
|
|
73
|
-
endOffset: lineOffset + lineSegment.relEnd,
|
|
74
|
-
};
|
|
75
|
-
return result;
|
|
76
|
-
}
|
|
77
|
-
function findNextWordText({ text, offset }) {
|
|
78
|
-
const reg = new RegExp(regExWordsAndDigits);
|
|
79
|
-
reg.lastIndex = offset;
|
|
80
|
-
const m = reg.exec(text);
|
|
81
|
-
if (!m) {
|
|
82
|
-
return {
|
|
83
|
-
text: '',
|
|
84
|
-
offset: offset + text.length,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
// Skip numeric literals.
|
|
88
|
-
if (regExNumericLiteral.test(m[0])) {
|
|
89
|
-
return findNextWordText({ text, offset: offset + m[0].length });
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
text: m[0],
|
|
93
|
-
offset: m.index,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
function generateWordBreaks(line, options) {
|
|
97
|
-
const camelBreaks = genWordBreakCamel(line);
|
|
98
|
-
const symbolBreaks = genSymbolBreaks(line);
|
|
99
|
-
const optionalBreaks = genOptionalWordBreaks(line, options.optionalWordBreakCharacters);
|
|
100
|
-
return mergeSortedBreaks(...camelBreaks, ...symbolBreaks, ...optionalBreaks);
|
|
101
|
-
}
|
|
102
|
-
function offsetRegEx(reg, offset) {
|
|
103
|
-
const r = new RegExp(reg);
|
|
104
|
-
r.lastIndex = offset;
|
|
105
|
-
return r;
|
|
106
|
-
}
|
|
107
|
-
function genWordBreakCamel(line) {
|
|
108
|
-
const breaksCamel1 = [];
|
|
109
|
-
const text = line.line.text.slice(0, line.relEnd);
|
|
110
|
-
// lower,Upper: camelCase -> camel|Case
|
|
111
|
-
for (const m of text.matchAll(offsetRegEx(regExSplitWords, line.relStart))) {
|
|
112
|
-
if (m.index === undefined)
|
|
113
|
-
break;
|
|
114
|
-
const i = m.index + m[1].length;
|
|
115
|
-
breaksCamel1.push({
|
|
116
|
-
offset: m.index,
|
|
117
|
-
breaks: [[i, i], ignoreBreak],
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
const breaksCamel2 = [];
|
|
121
|
-
// cspell:ignore ERRORC
|
|
122
|
-
// Upper,Upper,lower: ERRORCodes -> ERROR|Codes, ERRORC|odes
|
|
123
|
-
for (const m of text.matchAll(offsetRegEx(regExSplitWords2, line.relStart))) {
|
|
124
|
-
if (m.index === undefined)
|
|
125
|
-
break;
|
|
126
|
-
const i = m.index + m[1].length;
|
|
127
|
-
const j = i + m[3].length;
|
|
128
|
-
breaksCamel2.push({
|
|
129
|
-
offset: m.index,
|
|
130
|
-
breaks: [[i, i], [j, j], ignoreBreak],
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return [breaksCamel1, breaksCamel2];
|
|
134
|
-
}
|
|
135
|
-
function calcBreaksForRegEx(line, reg, calcBreak) {
|
|
136
|
-
const sb = [];
|
|
137
|
-
const text = line.line.text.slice(0, line.relEnd);
|
|
138
|
-
for (const m of text.matchAll(offsetRegEx(reg, line.relStart))) {
|
|
139
|
-
const b = calcBreak(m);
|
|
140
|
-
if (b) {
|
|
141
|
-
sb.push(b);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return sb;
|
|
145
|
-
}
|
|
146
|
-
function genOptionalWordBreaks(line, optionalBreakCharacters) {
|
|
147
|
-
function calcBreaks(m) {
|
|
148
|
-
const i = m.index;
|
|
149
|
-
if (i === undefined)
|
|
150
|
-
return;
|
|
151
|
-
const j = i + m[0].length;
|
|
152
|
-
return {
|
|
153
|
-
offset: i,
|
|
154
|
-
breaks: [
|
|
155
|
-
[i, j], // Remove the characters
|
|
156
|
-
ignoreBreak,
|
|
157
|
-
],
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
const breaks = [
|
|
161
|
-
calcBreaksForRegEx(line, regExDanglingQuote, calcBreaks),
|
|
162
|
-
calcBreaksForRegEx(line, regExTrailingEndings, calcBreaks),
|
|
163
|
-
];
|
|
164
|
-
if (optionalBreakCharacters) {
|
|
165
|
-
const regex = new RegExp(`[${escapeRegEx(optionalBreakCharacters)}]`, 'gu');
|
|
166
|
-
breaks.push(calcBreaksForRegEx(line, regex, calcBreaks));
|
|
167
|
-
}
|
|
168
|
-
return breaks;
|
|
169
|
-
}
|
|
170
|
-
function genSymbolBreaks(line) {
|
|
171
|
-
function calcBreaks(m) {
|
|
172
|
-
const i = m.index;
|
|
173
|
-
if (i === undefined)
|
|
174
|
-
return;
|
|
175
|
-
const j = i + m[0].length;
|
|
176
|
-
return {
|
|
177
|
-
offset: i,
|
|
178
|
-
breaks: [
|
|
179
|
-
[i, j], // Remove the characters
|
|
180
|
-
[i, i], // keep characters with word to right
|
|
181
|
-
[j, j], // keep characters with word to left
|
|
182
|
-
ignoreBreak,
|
|
183
|
-
],
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
return [
|
|
187
|
-
calcBreaksForRegEx(line, regExPossibleWordBreaks, calcBreaks),
|
|
188
|
-
calcBreaksForRegEx(line, /\d+/g, calcBreaks),
|
|
189
|
-
calcBreaksForRegEx(line, regExEscapeCharacters, calcBreaks),
|
|
190
|
-
];
|
|
191
|
-
}
|
|
192
|
-
function splitIntoWords(lineSeg, breaks, has) {
|
|
193
|
-
const maxIndex = lineSeg.relEnd;
|
|
194
|
-
const maxAttempts = 1000;
|
|
195
|
-
const knownPathsByIndex = new Map();
|
|
196
|
-
/**
|
|
197
|
-
* Create a set of possible candidate to consider
|
|
198
|
-
* @param p - prev candidate that lead to this one
|
|
199
|
-
* @param i - offset within the string
|
|
200
|
-
* @param bi - current index into the set of breaks
|
|
201
|
-
* @param currentCost - current cost accrued
|
|
202
|
-
*/
|
|
203
|
-
function makeCandidates(p, i, bi, currentCost) {
|
|
204
|
-
const len = maxIndex;
|
|
205
|
-
while (bi < breaks.length && breaks[bi].offset < i) {
|
|
206
|
-
bi += 1;
|
|
207
|
-
}
|
|
208
|
-
if (bi >= breaks.length) {
|
|
209
|
-
return [];
|
|
210
|
-
}
|
|
211
|
-
const br = breaks[bi];
|
|
212
|
-
function c(bp) {
|
|
213
|
-
const d = bp.length < 2 ? len - i : (bp[0] - i) * 0.5 + len - bp[1];
|
|
214
|
-
const ec = currentCost + d;
|
|
215
|
-
return {
|
|
216
|
-
p,
|
|
217
|
-
i,
|
|
218
|
-
bi,
|
|
219
|
-
bp,
|
|
220
|
-
c: currentCost,
|
|
221
|
-
ec,
|
|
222
|
-
text: undefined,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
return br.breaks.map(c);
|
|
226
|
-
}
|
|
227
|
-
function checkTextOffset(text, offset) {
|
|
228
|
-
const valid = has({ text, offset });
|
|
229
|
-
return {
|
|
230
|
-
text,
|
|
231
|
-
offset,
|
|
232
|
-
isFound: valid,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
function compare(a, b) {
|
|
236
|
-
return a.ec - b.ec || b.i - a.i;
|
|
237
|
-
}
|
|
238
|
-
function pathToWords(node) {
|
|
239
|
-
const results = [];
|
|
240
|
-
for (let p = node; p; p = p.n) {
|
|
241
|
-
if (p.text) {
|
|
242
|
-
results.push(p.text);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return results;
|
|
246
|
-
}
|
|
247
|
-
function addToKnownPaths(candidate, path) {
|
|
248
|
-
for (let can = candidate; can !== undefined; can = can.p) {
|
|
249
|
-
const t = can.text;
|
|
250
|
-
const i = can.i;
|
|
251
|
-
const cost = (!t || t.isFound ? 0 : t.text.length) + (path?.c ?? 0);
|
|
252
|
-
const exitingPath = knownPathsByIndex.get(i);
|
|
253
|
-
// Keep going only if this is a better candidate than the existing path
|
|
254
|
-
if (exitingPath && exitingPath.c <= cost) {
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
|
-
const node = {
|
|
258
|
-
n: path,
|
|
259
|
-
i,
|
|
260
|
-
c: cost,
|
|
261
|
-
text: t,
|
|
262
|
-
};
|
|
263
|
-
knownPathsByIndex.set(i, node);
|
|
264
|
-
path = node;
|
|
265
|
-
}
|
|
266
|
-
return path;
|
|
267
|
-
}
|
|
268
|
-
let maxCost = lineSeg.relEnd - lineSeg.relStart;
|
|
269
|
-
const candidates = new PairingHeap(compare);
|
|
270
|
-
const text = lineSeg.line.text;
|
|
271
|
-
candidates.append(makeCandidates(undefined, lineSeg.relStart, 0, 0));
|
|
272
|
-
let attempts = 0;
|
|
273
|
-
let bestPath;
|
|
274
|
-
while (maxCost && candidates.length && attempts++ < maxAttempts) {
|
|
275
|
-
/** Best Candidate Index */
|
|
276
|
-
const best = candidates.dequeue();
|
|
277
|
-
if (!best || best.c >= maxCost) {
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
// Does it have a split?
|
|
281
|
-
if (best.bp.length) {
|
|
282
|
-
// yes
|
|
283
|
-
const i = best.bp[0];
|
|
284
|
-
const j = best.bp[1];
|
|
285
|
-
const t = i > best.i ? checkTextOffset(text.slice(best.i, i), best.i) : undefined;
|
|
286
|
-
const cost = !t || t.isFound ? 0 : t.text.length;
|
|
287
|
-
const mc = maxIndex - j;
|
|
288
|
-
best.c += cost;
|
|
289
|
-
best.ec = best.c + mc;
|
|
290
|
-
best.text = t;
|
|
291
|
-
const possiblePath = knownPathsByIndex.get(j);
|
|
292
|
-
if (possiblePath) {
|
|
293
|
-
// We found a known apply to candidate
|
|
294
|
-
const f = addToKnownPaths(best, possiblePath);
|
|
295
|
-
bestPath = !bestPath || (f && f.c < bestPath.c) ? f : bestPath;
|
|
296
|
-
}
|
|
297
|
-
else if (best.c < maxCost) {
|
|
298
|
-
const c = makeCandidates(t ? best : best.p, j, best.bi + 1, best.c);
|
|
299
|
-
candidates.append(c);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
// It is a pass through
|
|
304
|
-
const c = makeCandidates(best.p, best.i, best.bi + 1, best.c);
|
|
305
|
-
candidates.append(c);
|
|
306
|
-
if (!c.length) {
|
|
307
|
-
const t = maxIndex > best.i ? checkTextOffset(text.slice(best.i, maxIndex), best.i) : undefined;
|
|
308
|
-
const cost = !t || t.isFound ? 0 : t.text.length;
|
|
309
|
-
best.c += cost;
|
|
310
|
-
best.ec = best.c;
|
|
311
|
-
best.text = t;
|
|
312
|
-
const segText = t || best.p?.text || checkTextOffset('', best.i);
|
|
313
|
-
const can = t ? { ...best, text: segText } : { ...best, ...best.p, text: segText };
|
|
314
|
-
const f = addToKnownPaths(can, undefined);
|
|
315
|
-
bestPath = !bestPath || (f && f.c < bestPath.c) ? f : bestPath;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
if (bestPath && bestPath.c < maxCost) {
|
|
319
|
-
maxCost = bestPath.c;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return pathToWords(bestPath);
|
|
323
|
-
}
|
|
324
|
-
function mergeSortedBreaks(...maps) {
|
|
325
|
-
return maps.flat().sort((a, b) => a.offset - b.offset);
|
|
326
|
-
}
|
|
327
|
-
export const __testing__ = {
|
|
328
|
-
generateWordBreaks,
|
|
329
|
-
findNextWordText,
|
|
330
|
-
};
|
|
331
|
-
//# sourceMappingURL=wordSplitter.js.map
|
|
File without changes
|