diff --git a/lib/routes/joneslanglasalle/index.ts b/lib/routes/joneslanglasalle/index.ts new file mode 100644 index 00000000000000..d58af1c910e62f --- /dev/null +++ b/lib/routes/joneslanglasalle/index.ts @@ -0,0 +1,310 @@ +import path from 'node:path'; + +import { type CheerioAPI, type Cheerio, type Element, load } from 'cheerio'; +import { type Context } from 'hono'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; + +import { art } from '@/utils/render'; +import cache from '@/utils/cache'; +import { getCurrentPath } from '@/utils/helpers'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +const __dirname = getCurrentPath(import.meta.url); + +const cleanHtml = (html: string, preservedTags: string[]): string => { + const $ = load(html); + + $('div.informationbox').remove(); + $('div.contributors').remove(); + + $('*') + .not(preservedTags.join(', ')) + .contents() + .filter((_, el) => el.type === 'text') + .remove(); + + $('*') + .not(preservedTags.join(', ')) + .filter((_, el) => $(el).children().length === 0) + .remove(); + + return $.html() || ''; +}; + +export const handler = async (ctx: Context): Promise => { + const { language: lang = 'zh', category = 'trends-and-insights' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '3', 10); + + const rootUrl: string = 'https://www.joneslanglasalle.com.cn'; + const targetUrl: string = new URL(`${lang}/${category}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'en'; + + let items: DataItem[] = $('div.ti-title') + .slice(0, limit) + .toArray() + .map((item): DataItem => { + const $item: Cheerio = $(item); + const aEl = $item.closest('a'); + + const title: string = $item.text(); + const link: string | undefined = aEl.prop('href'); + + const description: string = art(path.join(__dirname, 'templates/description.art'), { + intro: aEl.find('p.ti-teaser').text(), + }); + + const image: string | undefined = aEl.find('div.ti-image-container img').prop('src') ? new URL(aEl.find('div.ti-image-container img').prop('src') as string, rootUrl).href : undefined; + + return { + title, + description, + pubDate: parseDate(aEl.find('span.ti-date').text(), ['MM月DD日', 'MMMM DD']), + link: link ? new URL(link, rootUrl).href : undefined, + category: [aEl.find('span.ti-type').text()].filter(Boolean), + content: { + html: description, + text: aEl.find('p.ti-teaser').text(), + }, + image, + banner: image, + language, + }; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link && typeof item.link !== 'string') { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + try { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('meta[property="og:title"]').prop('content'); + const guid: string = $$('meta[property="og:url"]').prop('content'); + const image: string | undefined = $$('meta[property="og:image"]').prop('content'); + + const pubDate: Date = parseDate($$('div.publicationdate').text().trim(), ['YYYY 年MM 月DD 日', 'MMMMDD,YYYY']); + + const author: DataItem['author'] = $$('div.contributors ul li') + .toArray() + .map((el) => ({ + name: $$(el).text(), + })); + + const media: Record> = {}; + + $$('picture').each((_, el) => { + const $$el = $$(el); + + const src = $$el.find('source').last().prop('srcset') ? new URL($$el.find('source').last().prop('srcset') as string, rootUrl).href : undefined; + + if (src) { + $$el.replaceWith( + art(path.join(__dirname, 'templates/description.art'), { + images: [ + { + src, + }, + ], + }) + ); + + const mediaType: string | undefined = src.split(/\./).pop(); + + if (mediaType) { + media[mediaType] = { url: src }; + } + } + }); + + const extraLinks = $$('div.related-content a.content-card') + .toArray() + .map((el) => { + const $$el: Cheerio = $$(el); + + return { + url: new URL($$el.prop('href') as string, rootUrl).href, + type: 'related', + content_html: $$el.find('div.content-card__body').html(), + }; + }) + .filter((link): link is { url: string; type: string; content_html: string } => true); + + const description: string = art(path.join(__dirname, 'templates/description.art'), { + description: cleanHtml($$('div.page-section').eq(1).html() ?? $$('div.copy-block').html() ?? '', ['div.richtext p', 'h3', 'h4', 'h5', 'h6', 'figure', 'img', 'ul', 'li', 'span', 'b']), + }); + + return { + title, + description, + pubDate, + category: $$('meta[property="article:tag"]').prop('content').split(/,\s/), + author, + guid, + id: guid, + content: { + html: description, + text: description, + }, + image, + banner: image, + language, + media: Object.keys(media).length > 0 ? media : undefined, + _extra: { + links: extraLinks.length > 0 ? extraLinks : undefined, + }, + }; + } catch { + return item; + } + }); + }) + ) + ).filter((_): _ is DataItem => true); + + const title = $('title').text(); + const feedImage = $('img.logo').prop('src') ? new URL($('img.logo').prop('src') as string, rootUrl).href : undefined; + + return { + title, + description: $('meta[property="og:description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author: title.split(/\|/).pop(), + language, + id: $('meta[property="og:url"]').prop('content'), + }; +}; + +export const route: Route = { + path: '/:language?/:category{.+}?', + name: 'Trends & Insights', + url: 'joneslanglasalle.com.cn', + maintainers: ['nczitzk'], + handler, + example: '/joneslanglasalle/en/trends-and-insights', + parameters: { + language: 'Language, `zh` by default', + category: 'Category, `trends-and-insights` by default', + }, + description: `:::tip +If you subscribe to [Trends & Insights](https://www.joneslanglasalle.com.cn/en/trends-and-insights),where the URL is \`https://www.joneslanglasalle.com.cn/en/trends-and-insights\`, extract the part \`https://joneslanglasalle.com.cn/\` to the end. Use \`zh\` and \`trends-and-insights\` as the parameters to fill in. Therefore, the route will be [\`/joneslanglasalle/trends-and-insights/en/trends-and-insights\`](https://rsshub.app/joneslanglasalle/trends-and-insights/en/trends-and-insights). +::: + +| Category | ID | +| --------- | ----------------------------- | +| Latest | trends-and-insights | +| Workplace | trends-and-insights/workplace | +| Investor | trends-and-insights/investor | +| Cities | trends-and-insights/cities | +| Research | trends-and-insights/research | +`, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['joneslanglasalle.com.cn/:language/:category'], + target: (params) => { + const language = params.language; + const category = params.category; + + return language ? `/${language}${category ? `/${category}` : ''}` : ''; + }, + }, + { + title: 'Latest', + source: ['joneslanglasalle.com.cn/en/trends-and-insights'], + target: '/en/trends-and-insights', + }, + { + title: 'Workplace', + source: ['joneslanglasalle.com.cn/en/trends-and-insights/workplace'], + target: '/en/trends-and-insights/workplace', + }, + { + title: 'Investor', + source: ['joneslanglasalle.com.cn/en/trends-and-insights/investor'], + target: '/en/trends-and-insights/investor', + }, + { + title: 'Cities', + source: ['joneslanglasalle.com.cn/en/trends-and-insights/cities'], + target: '/en/trends-and-insights/cities', + }, + { + title: 'Research', + source: ['joneslanglasalle.com.cn/en/trends-and-insights/research'], + target: '/en/trends-and-insights/research', + }, + { + title: '房地产趋势与洞察', + source: ['joneslanglasalle.com.cn/zh/trends-and-insights'], + target: '/zh/trends-and-insights', + }, + { + title: '办公空间', + source: ['joneslanglasalle.com.cn/zh/trends-and-insights/workplace'], + target: '/zh/trends-and-insights/workplace', + }, + { + title: '投资者', + source: ['joneslanglasalle.com.cn/zh/trends-and-insights/investor'], + target: '/zh/trends-and-insights/investor', + }, + { + title: '城市', + source: ['joneslanglasalle.com.cn/zh/trends-and-insights/cities'], + target: '/zh/trends-and-insights/cities', + }, + { + title: '研究报告', + source: ['joneslanglasalle.com.cn/zh/trends-and-insights/research'], + target: '/zh/trends-and-insights/research', + }, + ], + view: ViewType.Articles, + + zh: { + path: '/:language?/:category{.+}?', + name: '房地产趋势与洞察', + url: 'joneslanglasalle.com.cn', + maintainers: ['nczitzk'], + handler, + example: '/joneslanglasalle/zh/trends-and-insights', + parameters: { + language: '语言,默认为 `zh`,可在对应分类页 URL 中找到', + category: '分类,默认为 `trends-and-insights`,可在对应分类页 URL 中找到', + }, + description: `:::tip +若订阅 [房地产趋势与洞察](https://www.joneslanglasalle.com.cn/zh/trends-and-insights),网址为 \`https://www.joneslanglasalle.com.cn/zh/trends-and-insights\`,请截取 \`https://joneslanglasalle.com.cn/\` 到末尾的部分 \`zh\` 和 \`trends-and-insights\` 作为 \`language\` 和 \`category\` 参数填入,此时目标路由为 [\`/joneslanglasalle/zh/trends-and-insights\`](https://rsshub.app/joneslanglasalle/zh/trends-and-insights)。 +::: + +| 分类名称 | 分类 ID | +| ---------- | ----------------------------- | +| 趋势及洞察 | trends-and-insights | +| 办公空间 | trends-and-insights/workplace | +| 投资者 | trends-and-insights/investor | +| 城市 | trends-and-insights/cities | +| 研究报告 | trends-and-insights/research | +`, + }, +}; diff --git a/lib/routes/joneslanglasalle/namespace.ts b/lib/routes/joneslanglasalle/namespace.ts new file mode 100644 index 00000000000000..2167344f3b2f3a --- /dev/null +++ b/lib/routes/joneslanglasalle/namespace.ts @@ -0,0 +1,13 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Jones Lang LaSalle', + url: 'joneslanglasalle.com.cn', + categories: ['new-media'], + description: 'JLL is a global real estate services firm in commercial property and investment management, providing services for real estate owners, occupiers and investors.', + lang: 'zh-CN', + zh: { + name: '仲量联行JLL', + description: '仲量联行JLL是全球领先的房地产专业服务和投资管理公司,为企业、房地产业主、投资者及政府提供各类资产的施工、租赁、管理、投资咨询服务。仲量联行也致力于高质量城市发展、打造理想空间、提供可持续的房地产解决方案。', + }, +}; diff --git a/lib/routes/joneslanglasalle/templates/description.art b/lib/routes/joneslanglasalle/templates/description.art new file mode 100644 index 00000000000000..aced21ab986b46 --- /dev/null +++ b/lib/routes/joneslanglasalle/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if !videos?.[0]?.src && image?.src }} +
+ {{ image.alt }} +
+ {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} +
{{ intro }}
+{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file