Skip to content

Commit cca333b

Browse files
committed
fix(dicts): correct cobuild select label
1 parent 5d711c1 commit cca333b

3 files changed

Lines changed: 308 additions & 34 deletions

File tree

src/components/dictionaries/cobuild/engine.ts

Lines changed: 181 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,43 @@ import {
1414
} from '../helpers'
1515
import { getStaticSpeaker } from '@/components/Speaker'
1616

17+
const HOST = 'https://www.collinsdictionary.com'
18+
const COLLINS_TYPE_PREFIX = 'definition.title.type.'
19+
20+
const SECTION_TYPE_LABELS: { [key: string]: string } = {
21+
cobuild: 'COBUILD',
22+
ced: 'Collins English Dictionary',
23+
english: 'Collins English Dictionary',
24+
american: 'American English Dictionary',
25+
aed: 'American English Dictionary',
26+
learner: 'COBUILD',
27+
penguin: 'Penguin Dictionary',
28+
esp_ret: 'Retail',
29+
examples: 'Examples',
30+
idioms: 'Idioms',
31+
collos: 'Collocations'
32+
}
33+
34+
const SKIPPED_SECTION_KEYS = new Set([
35+
'video',
36+
'trends',
37+
'wordlists',
38+
'translation',
39+
'translations'
40+
])
41+
42+
const SKIPPED_SECTION_LABELS = new Set([
43+
'video pronunciation',
44+
'word lists',
45+
'word usage trends',
46+
'translations',
47+
'英语词汇表',
48+
'趋势'
49+
])
50+
1751
export const getSrcPage: GetSrcPageFunction = text => {
1852
return (
19-
`https://www.collinsdictionary.com/dictionary/english/` +
53+
`${HOST}/dictionary/english/` +
2054
encodeURIComponent(text.replace(/\s+/g, '-'))
2155
)
2256
}
@@ -114,44 +148,37 @@ async function handleDOM(
114148
}
115149
const audio: { uk?: string; us?: string } = {}
116150

117-
result.sections = [
118-
...doc.querySelectorAll<HTMLDivElement>(`[data-type-block]`)
119-
]
120-
.filter($section => {
121-
const type = $section.dataset.typeBlock || ''
122-
return (
123-
type &&
124-
type !== 'Video' &&
125-
type !== 'Trends' &&
126-
type !== '英语词汇表' &&
127-
type !== '趋势'
128-
)
151+
result.sections = getSectionNodes(doc)
152+
.filter(({ meta }) => {
153+
const type = getCleanLabel(meta.dataset.typeBlock)
154+
return !!type && !shouldSkipSectionType(type)
129155
})
130-
.map($section => {
131-
const type = $section.dataset.typeBlock || ''
132-
const title = $section.dataset.titleBlock || ''
133-
const num = $section.dataset.numBlock || ''
156+
.map(({ meta, content: $section }, i) => {
157+
const rawType = getCleanLabel(meta.dataset.typeBlock)
158+
const type = normalizeSectionType(rawType)
159+
const title = getSectionTitle(meta, $section, type)
160+
const num = getCleanLabel(
161+
meta.dataset.numBlock || $section.dataset.numBlock
162+
)
134163
const id = type + title + num
135164
const className = $section.className || ''
165+
const mp3 = getAudio($section)
136166

137-
if (type === 'Learner') {
167+
if (isCobuildSection(rawType)) {
138168
// const $frequency = $section.querySelector<HTMLSpanElement>('.word-frequency-img')
139169
// if ($frequency) {
140170
// const star = Number($frequency.dataset.band)
141171
// if (star) {
142172
// result.star = star
143173
// }
144174
// }
145-
if (!audio.uk) {
146-
const mp3 = getAudio($section)
147-
if (mp3) {
148-
audio.uk = mp3
149-
}
175+
if (!audio.uk && mp3) {
176+
audio.uk = mp3
150177
}
151-
} else if (type === 'English') {
152-
audio.uk = getAudio($section)
153-
} else if (type === 'American') {
154-
audio.us = getAudio($section)
178+
} else if (isAmericanSection(rawType) && mp3) {
179+
audio.us = mp3
180+
} else if (mp3 && (isEnglishSection(rawType) || !audio.uk)) {
181+
audio.uk = mp3
155182
}
156183

157184
const $video = $section.querySelector<HTMLDivElement>('#videos .video')
@@ -185,12 +212,12 @@ async function handleDOM(
185212
.forEach(externalLink)
186213

187214
return {
188-
id,
215+
id: id || String(i),
189216
className,
190217
type,
191218
title,
192219
num,
193-
content: getInnerHTML('https://www.collinsdictionary.com', $section, {
220+
content: getInnerHTML(HOST, $section, {
194221
transform
195222
})
196223
}
@@ -203,6 +230,35 @@ async function handleDOM(
203230
return handleNoResult()
204231
}
205232

233+
function getSectionNodes(
234+
doc: Document
235+
): Array<{ meta: HTMLElement; content: HTMLElement }> {
236+
const sections: Array<{ meta: HTMLElement; content: HTMLElement }> = []
237+
const seen = new Set<HTMLElement>()
238+
239+
doc.querySelectorAll<HTMLElement>('[data-type-block]').forEach(meta => {
240+
const content = getSectionContentNode(meta)
241+
if (!seen.has(content)) {
242+
seen.add(content)
243+
sections.push({ meta, content })
244+
}
245+
})
246+
247+
return sections
248+
}
249+
250+
function getSectionContentNode($node: HTMLElement): HTMLElement {
251+
if (!$node.classList.contains('cB-h')) {
252+
return $node
253+
}
254+
255+
return (
256+
$node.closest<HTMLElement>(
257+
'.entry.dictionary.cB, .entry.cB, .asset, .cB'
258+
) || $node
259+
)
260+
}
261+
206262
function getAudio($section: HTMLElement): string | undefined {
207263
const $audio = $section.querySelector<HTMLAnchorElement>(
208264
'.pron .audio_play_button'
@@ -214,3 +270,99 @@ function getAudio($section: HTMLElement): string | undefined {
214270
}
215271
}
216272
}
273+
274+
function shouldSkipSectionType(type: string): boolean {
275+
const key = getSectionTypeKey(type)
276+
const label = getCleanLabel(type).toLowerCase()
277+
278+
return (
279+
SKIPPED_SECTION_KEYS.has(key) ||
280+
SKIPPED_SECTION_LABELS.has(label) ||
281+
/^translations?(?:\s+of\b)?/.test(label)
282+
)
283+
}
284+
285+
function normalizeSectionType(type: string): string {
286+
const key = getSectionTypeKey(type)
287+
if (SECTION_TYPE_LABELS[key]) {
288+
return SECTION_TYPE_LABELS[key]
289+
}
290+
291+
if (key !== type.toLowerCase()) {
292+
return key
293+
.split(/[_\s-]+/)
294+
.filter(Boolean)
295+
.map(word => word[0].toUpperCase() + word.slice(1))
296+
.join(' ')
297+
}
298+
299+
return type
300+
}
301+
302+
function getSectionTypeKey(type: string): string {
303+
const lowerType = type.toLowerCase()
304+
return lowerType.startsWith(COLLINS_TYPE_PREFIX)
305+
? lowerType.slice(COLLINS_TYPE_PREFIX.length)
306+
: lowerType
307+
}
308+
309+
function getSectionTitle(
310+
$meta: HTMLElement,
311+
$section: HTMLElement,
312+
type: string
313+
): string {
314+
const title = normalizeSectionTitle(
315+
getCleanLabel($meta.dataset.titleBlock) ||
316+
getCleanLabel(getTitleElement($meta)) ||
317+
getCleanLabel(getTitleElement($section))
318+
)
319+
320+
return title && !title.toLowerCase().startsWith(type.toLowerCase())
321+
? title
322+
: ''
323+
}
324+
325+
function getTitleElement($section: HTMLElement): Element | null {
326+
return $section.querySelector(
327+
[
328+
'.cB-h .entry_title',
329+
'.cB-h .h2_entry',
330+
'.entry_title',
331+
'.h2_entry',
332+
'.content-box-header .h2_entry',
333+
'.content-box-header h2'
334+
].join(',')
335+
)
336+
}
337+
338+
function normalizeSectionTitle(title: string): string {
339+
return getSectionTypeKey(title) !== title.toLowerCase()
340+
? normalizeSectionType(title)
341+
: title
342+
}
343+
344+
function getCleanLabel(input?: string | Element | null): string {
345+
const text =
346+
typeof input === 'string' ? input : input ? input.textContent || '' : ''
347+
348+
return text
349+
.replace(/<[^>]*>/g, '')
350+
.replace(/\{\d+\}/g, '')
351+
.replace(/\s+/g, ' ')
352+
.trim()
353+
}
354+
355+
function isCobuildSection(type: string): boolean {
356+
const key = getSectionTypeKey(type)
357+
return key === 'cobuild' || key === 'learner' || type === 'Learner'
358+
}
359+
360+
function isEnglishSection(type: string): boolean {
361+
const key = getSectionTypeKey(type)
362+
return key === 'ced' || key === 'english' || type === 'English'
363+
}
364+
365+
function isAmericanSection(type: string): boolean {
366+
const key = getSectionTypeKey(type)
367+
return key === 'american' || key === 'aed' || type === 'American'
368+
}

test/specs/components/dictionaries/cobuild/engine.spec.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,37 @@ describe('Dict/COBUILD/engine', () => {
2323
const profile = getDefaultProfile() as ProfileMutable
2424
return retry(() =>
2525
search('love', getDefaultConfig(), profile, { isPDF: false }).then(
26-
searchResult => {
27-
expect(searchResult.result).toBeTruthy()
26+
({ result, audio }) => {
27+
expect(result).toBeTruthy()
28+
29+
if (result.type !== 'collins') {
30+
throw new Error('Expected Collins result')
31+
}
32+
33+
expect(audio && audio.uk).toBe('https://example.com/ced.mp3')
34+
expect(result.sections.map(section => section.type)).toEqual([
35+
'COBUILD',
36+
'Collins English Dictionary',
37+
'Penguin Dictionary',
38+
'Examples',
39+
'Retail',
40+
'Idioms',
41+
'Collocations'
42+
])
43+
expect(
44+
result.sections.find(section => section.type === 'Idioms')!.content
45+
).toContain('pick holes in something')
46+
expect(
47+
result.sections.find(section => section.type === 'Collocations')!
48+
.content
49+
).toContain('pick a favorite')
50+
expect(
51+
result.sections.some(section =>
52+
/definition\.title\.type|Video pronunciation|Word lists|Word usage trends|Translations/.test(
53+
section.type + section.title
54+
)
55+
)
56+
).toBe(false)
2857
}
2958
)
3059
)

0 commit comments

Comments
 (0)