@whiteslove/parsing-lexicon 0.5.9 → 0.5.12
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/package.json +1 -1
- package/src/hiring-advanced.js +2 -0
- package/src/hiring-context.js +9 -2
- package/src/hiring-source-semantics.d.ts +7 -0
- package/src/hiring-source-semantics.js +34 -1
- package/src/hiring-work-semantics.d.ts +2 -0
- package/src/hiring-work-semantics.js +18 -0
- package/src/money.js +4 -0
package/package.json
CHANGED
package/src/hiring-advanced.js
CHANGED
|
@@ -130,6 +130,7 @@ export function parseExperience(value) {
|
|
|
130
130
|
|
|
131
131
|
const range = text.match(RANGE_RE);
|
|
132
132
|
if (range) {
|
|
133
|
+
if (/(?:возраст|вік|age|yosh|ёш|vârsta|varsta)/iu.test(range[0]) && !/(?:опыт|experience|досвід|tajriba|тәжірибе)/iu.test(range[0])) return null;
|
|
133
134
|
const minYears = Number(range[1].replace(',', '.'));
|
|
134
135
|
const maxYears = Number(range[2].replace(',', '.'));
|
|
135
136
|
return Object.freeze({ requirement: modifier || 'required', minYears: Math.min(minYears, maxYears), maxYears: Math.max(minYears, maxYears) });
|
|
@@ -137,6 +138,7 @@ export function parseExperience(value) {
|
|
|
137
138
|
|
|
138
139
|
const single = text.match(MIN_RE);
|
|
139
140
|
if (single) {
|
|
141
|
+
if (/(?:возраст|вік|age|yosh|ёш|vârsta|varsta)/iu.test(single[0]) && !/(?:опыт|experience|досвід|tajriba|тәжірибе)/iu.test(single[0])) return null;
|
|
140
142
|
const years = Number(single[1].replace(',', '.'));
|
|
141
143
|
const isExact = !/(?:от|не\s+менее|minimum|min\.?|at\s+least|від|kamida|кемінде|\+)/iu.test(single[0]);
|
|
142
144
|
return Object.freeze({ requirement: modifier || 'required', minYears: years, maxYears: isExact ? years : null });
|
package/src/hiring-context.js
CHANGED
|
@@ -194,7 +194,7 @@ export const HIRING_SECTION_MARKERS = Object.freeze({
|
|
|
194
194
|
export const HIRING_NON_CONTENT_TERMS = Object.freeze([
|
|
195
195
|
group('vacancyDigest', { ru: ['дайджест вакансий', 'подборка вакансий', 'вакансии недели'], en: ['job digest', 'vacancy digest', 'jobs roundup'], uk: ['добірка вакансій'], ro: ['selecție de joburi'], uzLatn: ['vakansiyalar toplami'], uzCyrl: ['вакансиялар тўплами'], kk: ['вакансиялар топтамасы'] }),
|
|
196
196
|
group('recruitmentAd', { ru: ['поможем подобрать сотрудника', 'услуги рекрутера', 'закроем вакансию'], en: ['recruitment services', 'we find candidates', 'staffing services'], uk: ['послуги рекрутера'], ro: ['servicii de recrutare'], uzLatn: ['rekruting xizmati'], uzCyrl: ['рекрутинг хизмати'], kk: ['рекрутинг қызметтері'] }),
|
|
197
|
-
group('jobService', { ru: ['помогу найти работу', 'подбор вакансий', 'сервис поиска работы'], en: ['job search service', 'we help you find a job'], uk: ['допоможемо знайти роботу'], ro: ['serviciu de căutare job'], uzLatn: ['ish topishga yordam'], uzCyrl: ['иш топишга ёрдам'], kk: ['жұмыс табуға көмектесеміз'] }),
|
|
197
|
+
group('jobService', { ru: ['помогу найти работу', 'подбор вакансий', 'сервис поиска работы', 'каналы с вакансиями', 'выбирай свою сферу и подписывайся'], en: ['job search service', 'we help you find a job'], uk: ['допоможемо знайти роботу'], ro: ['serviciu de căutare job'], uzLatn: ['ish topishga yordam'], uzCyrl: ['иш топишга ёрдам'], kk: ['жұмыс табуға көмектесеміз'] }),
|
|
198
198
|
group('spam', { ru: ['подпишись на канал', 'заработок без вложений', 'доход без вложений', 'легкий заработок'], en: ['subscribe to our channel', 'earn money without investment', 'easy money'], uk: ['підпишись на канал', 'заробіток без вкладень'], ro: ['abonează-te la canal', 'câștig fără investiții'], uzLatn: ['kanalga obuna boling', 'sarmoyasiz daromad'], uzCyrl: ['каналга обуна бўлинг'], kk: ['арнаға жазылыңыз', 'инвестициясыз табыс'] }),
|
|
199
199
|
]);
|
|
200
200
|
|
|
@@ -207,8 +207,15 @@ const SECTION_PATTERNS = Object.freeze(Object.fromEntries(
|
|
|
207
207
|
Object.entries(HIRING_SECTION_MARKERS).map(([key, entry]) => [key, sectionPattern(entry)]),
|
|
208
208
|
));
|
|
209
209
|
|
|
210
|
+
const INLINE_SECTION_LABELS = Object.values(HIRING_SECTION_MARKERS)
|
|
211
|
+
.flatMap((entry) => aliasesOf(entry))
|
|
212
|
+
.sort((a, b) => b.length - a.length)
|
|
213
|
+
.map((value) => escapeRegex(value).replace(/\s+/g, '\\s+'))
|
|
214
|
+
.join('|');
|
|
215
|
+
const INLINE_SECTION_MARKER_RE = new RegExp(`([^\\p{L}\\p{N}\\s])(?=(?:${INLINE_SECTION_LABELS})\\s*[::—-])`, 'giu');
|
|
216
|
+
|
|
210
217
|
export function splitHiringSections(value) {
|
|
211
|
-
const text = String(value || '').replace(/\r/g, '');
|
|
218
|
+
const text = String(value || '').replace(/\r/g, '').replace(INLINE_SECTION_MARKER_RE, '\n$1');
|
|
212
219
|
const out = {};
|
|
213
220
|
let current = null;
|
|
214
221
|
for (const rawLine of text.split('\n')) {
|
|
@@ -6,6 +6,13 @@ export type VisaSponsorshipWording = 'offered' | 'notOffered' | null;
|
|
|
6
6
|
export function cleanHiringSourceText(value: unknown): string;
|
|
7
7
|
export function extractCandidateStructuredField(value: unknown, key: string, maxLength?: number): string | null;
|
|
8
8
|
export function extractJobStructuredField(value: unknown, key: string, maxLength?: number): string | null;
|
|
9
|
+
export type HiringVacancyEntry = Readonly<{ title: string; text: string }>;
|
|
10
|
+
export type SplitHiringVacancyPost = Readonly<{
|
|
11
|
+
prefix: string;
|
|
12
|
+
suffix: string;
|
|
13
|
+
entries: readonly HiringVacancyEntry[];
|
|
14
|
+
}>;
|
|
15
|
+
export function splitHiringVacancyEntries(value: unknown): SplitHiringVacancyPost;
|
|
9
16
|
export function extractCandidateDisplayName(value: unknown): string;
|
|
10
17
|
export function extractCandidateExperienceMentions(value: unknown): readonly CandidateExperienceMention[];
|
|
11
18
|
export function parseHiringSourceSalary(value: unknown): SalaryParseResult | null;
|
|
@@ -21,7 +21,7 @@ const FIELD_EXTRA_ALIASES = Object.freeze({
|
|
|
21
21
|
}),
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
const APPLICATION_MODAL_RE = /(?:отправить\s
|
|
24
|
+
const APPLICATION_MODAL_RE = /(?:отправить\s*резюме\s*)+(?:\[telegram\]\s*)?пол\s*мужской\s*женский\s*образование[\s\S]{0,500}?выберите\s*вакансию[\s\S]*/iu;
|
|
25
25
|
const LEAKED_STYLESHEET_START_RE = /:root\s*\{\s*--[\w-]+\s*:/iu;
|
|
26
26
|
const LEAKED_CSS_RULE_RE = /\s*(?:[.#][\w\\-]+|:root)(?:[^{}\n]{0,300})\{\s*(?:(?:--)?[\w-]+\s*:\s*[^;{}]+;?\s*)+\}\s*/giu;
|
|
27
27
|
|
|
@@ -62,6 +62,39 @@ export function extractJobStructuredField(value, key, maxLength = 220) {
|
|
|
62
62
|
return extractField(value, JOB_FIELD_TERMS[key], FIELD_EXTRA_ALIASES.job[key] || [], maxLength);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
const INLINE_VACANCY_HEADING_RE = /✔️?\s*([A-ZА-ЯЁЎҚҒҲІЇЄ][A-ZА-ЯЁЎҚҒҲІЇЄ\s()\/-]{2,100}?)(?=\s*(?:[•⏳✔📍✈📞]|$))/gu;
|
|
66
|
+
const SHARED_HIRING_CONTACT_RE = /(?:✈️?\s*Telegram\s*:|📞\s*Контакт\s*:)/iu;
|
|
67
|
+
|
|
68
|
+
/** Split a multi-vacancy source post into role-local blocks and shared context. */
|
|
69
|
+
export function splitHiringVacancyEntries(value) {
|
|
70
|
+
const text = cleanHiringSourceText(value);
|
|
71
|
+
if (!text) return Object.freeze({ prefix: '', suffix: '', entries: Object.freeze([]) });
|
|
72
|
+
|
|
73
|
+
const headings = [...text.matchAll(INLINE_VACANCY_HEADING_RE)]
|
|
74
|
+
.map((match) => ({ start: match.index ?? 0, title: match[1].replace(/\s+/g, ' ').trim() }));
|
|
75
|
+
if (headings.length < 2) return Object.freeze({ prefix: text, suffix: '', entries: Object.freeze([]) });
|
|
76
|
+
|
|
77
|
+
let suffix = '';
|
|
78
|
+
let contentEnd = text.length;
|
|
79
|
+
const lastStart = headings.at(-1).start;
|
|
80
|
+
const contact = SHARED_HIRING_CONTACT_RE.exec(text.slice(lastStart));
|
|
81
|
+
if (contact?.index != null) {
|
|
82
|
+
contentEnd = lastStart + contact.index;
|
|
83
|
+
suffix = text.slice(contentEnd).trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const entries = headings.map((heading, index) => {
|
|
87
|
+
const end = Math.min(headings[index + 1]?.start ?? contentEnd, contentEnd);
|
|
88
|
+
return Object.freeze({ title: heading.title, text: text.slice(heading.start, end).trim() });
|
|
89
|
+
}).filter(({ text: entryText }) => Boolean(entryText));
|
|
90
|
+
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
prefix: text.slice(0, headings[0].start).trim(),
|
|
93
|
+
suffix,
|
|
94
|
+
entries: Object.freeze(entries),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
65
98
|
export function extractCandidateDisplayName(value) {
|
|
66
99
|
const text = String(value || '');
|
|
67
100
|
const structured = extractCandidateName(text);
|
|
@@ -26,9 +26,11 @@ export type HiringWorkSchedule =
|
|
|
26
26
|
|
|
27
27
|
export type HiringProbationKind = 'probation' | 'noProbation' | 'paidProbation' | 'unpaidProbation';
|
|
28
28
|
export type HiringExperienceRequirement = 'noExperience' | 'experienceRequired';
|
|
29
|
+
export type HiringWorkTimeRange = Readonly<{ start: string; end: string }>;
|
|
29
30
|
|
|
30
31
|
export function detectEmploymentTypes(value: unknown): readonly HiringEmploymentType[];
|
|
31
32
|
export function detectWorkModes(value: unknown): readonly HiringWorkMode[];
|
|
32
33
|
export function detectWorkSchedules(value: unknown): readonly HiringWorkSchedule[];
|
|
34
|
+
export function extractWorkTimeRanges(value: unknown): readonly HiringWorkTimeRange[];
|
|
33
35
|
export function detectProbation(value: unknown): HiringProbationKind | null;
|
|
34
36
|
export function detectExperienceRequirement(value: unknown): HiringExperienceRequirement | null;
|
|
@@ -66,6 +66,24 @@ export function detectWorkSchedules(value) {
|
|
|
66
66
|
return Object.freeze(collectCanonical(String(value || ''), [...SCHEDULE_TERMS, ...WORK_SCHEDULE_EXTENSIONS]));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
const WORK_TIME_RANGE_RE = /(?:^|[^\p{L}\p{N}])(?:с|from|de\s+la)?\s*([01]?\d|2[0-3])[:.]([0-5]\d)\s*(?:[-–—]|до|to|până\s+la|pana\s+la|gacha)\s*([01]?\d|2[0-3])[:.]([0-5]\d)(?!\d)/giu;
|
|
70
|
+
|
|
71
|
+
/** Extract explicit working-time intervals without confusing them with contact hours. */
|
|
72
|
+
export function extractWorkTimeRanges(value) {
|
|
73
|
+
const text = String(value || '');
|
|
74
|
+
const ranges = [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const match of text.matchAll(WORK_TIME_RANGE_RE)) {
|
|
77
|
+
const start = `${match[1].padStart(2, '0')}:${match[2]}`;
|
|
78
|
+
const end = `${match[3].padStart(2, '0')}:${match[4]}`;
|
|
79
|
+
const key = `${start}-${end}`;
|
|
80
|
+
if (seen.has(key)) continue;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
ranges.push(Object.freeze({ start, end }));
|
|
83
|
+
}
|
|
84
|
+
return Object.freeze(ranges);
|
|
85
|
+
}
|
|
86
|
+
|
|
69
87
|
export function detectProbation(value) {
|
|
70
88
|
const text = String(value || '');
|
|
71
89
|
for (const entry of PROBATION_MATCH_ORDER) {
|
package/src/money.js
CHANGED
|
@@ -101,6 +101,10 @@ function rangeSearchText(text) {
|
|
|
101
101
|
for (const symbol of Object.keys(CURRENCY_SYMBOL_CANDIDATES)) {
|
|
102
102
|
normalized = normalized.split(symbol).join(' '.repeat(symbol.length));
|
|
103
103
|
}
|
|
104
|
+
// Some job boards render salary parts as separate UI tokens and join their
|
|
105
|
+
// accessible text with middle dots: "2 500 · - 3 000 · €". Treat those
|
|
106
|
+
// decorative separators as whitespace while preserving source offsets.
|
|
107
|
+
normalized = normalized.replace(/[·•]/gu, ' ');
|
|
104
108
|
return normalized;
|
|
105
109
|
}
|
|
106
110
|
|