@whiteslove/parsing-lexicon 0.5.8 → 0.5.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteslove/parsing-lexicon",
3
- "version": "0.5.8",
3
+ "version": "0.5.11",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for WhitesLove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,8 @@
38
38
  "./central-asia": "./src/central-asia.js",
39
39
  "./ukraine": "./src/ukraine.js",
40
40
  "./location-merge": "./src/location-merge.js",
41
- "./locations": "./src/locations.js",
41
+ "./locations": "./src/locations-runtime.js",
42
+ "./kg-location-extensions": "./src/kg-location-extensions.js",
42
43
  "./kz-location-extensions": "./src/kz-location-extensions.js",
43
44
  "./uz-location-extensions": "./src/uz-location-extensions.js",
44
45
  "./central-asia-locations": "./src/central-asia-locations.js",
@@ -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')) {
@@ -21,12 +21,15 @@ const FIELD_EXTRA_ALIASES = Object.freeze({
21
21
  }),
22
22
  });
23
23
 
24
- const APPLICATION_MODAL_RE = /(?:отправить\s+резюме\s+)?пол\s+мужской\s+женский\s+образование[\s\S]{0,500}?выберите\s+вакансию[\s\S]*/iu;
25
- const LEAKED_CSS_RULE_RE = /\s*[.#][\w\\-]+(?:\s+[.#][\w\\-]+)?(?::[\w-]+)?\s*\{\s*(?:[\w-]+\s*:\s*[^;{}]+;?\s*)+\}\s*/gu;
24
+ const APPLICATION_MODAL_RE = /(?:отправить\s*резюме\s*)+(?:\[telegram\]\s*)?пол\s*мужской\s*женский\s*образование[\s\S]{0,500}?выберите\s*вакансию[\s\S]*/iu;
25
+ const LEAKED_STYLESHEET_START_RE = /:root\s*\{\s*--[\w-]+\s*:/iu;
26
+ const LEAKED_CSS_RULE_RE = /\s*(?:[.#][\w\\-]+|:root)(?:[^{}\n]{0,300})\{\s*(?:(?:--)?[\w-]+\s*:\s*[^;{}]+;?\s*)+\}\s*/giu;
26
27
 
27
- /** Remove source-page controls and stylesheet fragments copied into vacancy text. */
28
+ /** Remove source-page controls and stylesheet fragments copied into hiring text. */
28
29
  export function cleanHiringSourceText(value) {
29
- return String(value || '')
30
+ const text = String(value || '');
31
+ const stylesheetStart = text.search(LEAKED_STYLESHEET_START_RE);
32
+ return (stylesheetStart >= 0 ? text.slice(0, stylesheetStart) : text)
30
33
  .replace(APPLICATION_MODAL_RE, '')
31
34
  .replace(LEAKED_CSS_RULE_RE, ' ')
32
35
  .replace(/[ \t]+(?=\n)/g, '')
@@ -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/index.js CHANGED
@@ -18,7 +18,8 @@ export * from './geography-detection.js';
18
18
  export * from './central-asia.js';
19
19
  export * from './ukraine.js';
20
20
  export * from './location-merge.js';
21
- export * from './locations.js';
21
+ export * from './locations-runtime.js';
22
+ export * from './kg-location-extensions.js';
22
23
  export * from './kz-location-extensions.js';
23
24
  export * from './uz-location-extensions.js';
24
25
  export * from './central-asia-locations.js';
@@ -0,0 +1,25 @@
1
+ import { aliasesToRegex } from './normalization.js';
2
+
3
+ const district = (name, aliases = []) => {
4
+ const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ return Object.freeze({
6
+ canonical: name,
7
+ name,
8
+ type: 'district',
9
+ entityType: 'district',
10
+ country: 'KG',
11
+ aliases: all,
12
+ re: aliasesToRegex(all),
13
+ });
14
+ };
15
+
16
+ export const KG_LOCATION_EXTENSIONS = Object.freeze({
17
+ Bishkek: Object.freeze({
18
+ districts: Object.freeze([
19
+ district('Pervomaisky', ['Первомайский', 'Первомайский район', 'Биринчи Май', 'Биринчи Май району', 'Birinchi May', 'Birinchi May district']),
20
+ district('Leninsky', ['Ленинский', 'Ленинский район', 'Ленин району', 'Lenin district']),
21
+ district('Oktyabrsky', ['Октябрьский', 'Октябрьский район', 'Октябрь району', 'Oktyabr district']),
22
+ district('Sverdlovsky', ['Свердловский', 'Свердловский район', 'Свердлов району', 'Sverdlov district']),
23
+ ]),
24
+ }),
25
+ });
@@ -0,0 +1,52 @@
1
+ import {
2
+ LOCATION_DICTIONARIES as BASE_LOCATION_DICTIONARIES,
3
+ UA_REGION_ENTRIES,
4
+ UA_SECONDARY_CITIES,
5
+ matchUkraineRegion,
6
+ matchUkraineSecondaryCity,
7
+ } from './locations.js';
8
+ import { LOCATION_LIST_KEYS, mergeLocationCountries } from './location-merge.js';
9
+ import { KG_LOCATION_EXTENSIONS } from './kg-location-extensions.js';
10
+
11
+ export { UA_REGION_ENTRIES, UA_SECONDARY_CITIES, matchUkraineRegion, matchUkraineSecondaryCity };
12
+
13
+ export const LOCATION_DICTIONARIES = Object.freeze({
14
+ ...BASE_LOCATION_DICTIONARIES,
15
+ KG: mergeLocationCountries(
16
+ BASE_LOCATION_DICTIONARIES.KG || {},
17
+ KG_LOCATION_EXTENSIONS,
18
+ ),
19
+ });
20
+
21
+ export function dictionaryFor(countryCode, city) {
22
+ return LOCATION_DICTIONARIES[countryCode]?.[city] || null;
23
+ }
24
+
25
+ export function locationCities(countryCode) {
26
+ return LOCATION_DICTIONARIES[countryCode] || Object.freeze({});
27
+ }
28
+
29
+ export function matchDictionaryLocation(text, countryCode, city = null) {
30
+ const country = locationCities(countryCode);
31
+ const cities = city && country[city] ? [[city, country[city]]] : Object.entries(country);
32
+ const value = String(text || '');
33
+ let best = null;
34
+
35
+ for (const [cityName, data] of cities) {
36
+ for (const type of LOCATION_LIST_KEYS) {
37
+ for (const entry of data[type] || []) {
38
+ const match = entry?.re?.exec(value);
39
+ if (!match) continue;
40
+ const start = match.index;
41
+ const end = start + match[0].length;
42
+ const containsBest = best && start <= best.start && end >= best.end && end - start > best.end - best.start;
43
+ if (best && !containsBest) continue;
44
+ best = { city: cityName, type, name: entry.name, aliases: entry.aliases, start, end };
45
+ }
46
+ }
47
+ }
48
+
49
+ if (!best) return null;
50
+ const { start: _start, end: _end, ...result } = best;
51
+ return result;
52
+ }