diff --git a/lib/v2/linkedin/cn/index.js b/lib/v2/linkedin/cn/index.js index 341c5f5d3..2958a1472 100644 --- a/lib/v2/linkedin/cn/index.js +++ b/lib/v2/linkedin/cn/index.js @@ -4,7 +4,7 @@ const siteUrl = 'https://www.linkedin.cn/incareer/jobs/search'; module.exports = async (ctx) => { const { title, jobs } = await parseSearchHit(ctx); - const items = await Promise.all(jobs.map((job) => parseJobPosting(job))); + const items = await Promise.all(jobs.map((job) => parseJobPosting(ctx, job))); ctx.state.data = { title: `领英 - ${title}`, link: siteUrl, diff --git a/lib/v2/linkedin/cn/renderer.js b/lib/v2/linkedin/cn/renderer.js new file mode 100644 index 000000000..52509e888 --- /dev/null +++ b/lib/v2/linkedin/cn/renderer.js @@ -0,0 +1,163 @@ +const text_tag = { + LINE_BREAK: 0, + INLINE_CODE: 1, + LEGACY_PUBLISHING_EMPHASIS: 2, + LIST_ITEM: 3, + LIST: 4, + STRIKETHROUGH: 5, + SUBSCRIPT: 6, + SUPERSCRIPT: 7, + UNDERLINE: 8, + BOLD: 9, + ITALIC: 10, + PARAGRAPH: 11, +}; + +class TreeNode { + constructor(attr) { + this.attr = attr; + this.start = attr.start; + this.end = attr.start + attr.length; + this.children = []; + } +} +class Ucs2Text { + constructor(text) { + if (Array.isArray(text)) { + this.string = null; + this.codePoints = text; + } else { + this.string = text; + const k = text.length; + const d = []; + + for (let l, e, m = 0; m < k; ) { + 55296 <= (l = text.charCodeAt(m++)) && 56319 >= l && m < k ? (56320 === (64512 & (e = text.charCodeAt(m++))) ? d.push(((1023 & l) << 10) + (1023 & e) + 65536) : (d.push(l), m--)) : d.push(l); + } + this.codePoints = d; + } + this.length = this.codePoints.length; + } + substring(start, end) { + let _len = this.length, + _start = start, + _end = end; + if (_end === 0) { + return new Ucs2Text(''); + } + if (((isNaN(_start) || 0 > _start) && (_start = 0), (isNaN(_end) || 0 > _end) && (_end = _len), _start > _len && (_start = _len), _end > _len && (_end = _len), _end < _start)) { + _len = [_start, _end]; + _end = _len[0]; + _start = _len[1]; + } + return new Ucs2Text(_start === _end ? '' : this.codePoints.slice(_start, _end)); + } + slice(a, b) { + return this.substring(a, b).toString(); + } + toString() { + return null !== this.string + ? this.string + : this.codePoints + .map((a) => { + let b = ''; + return 65535 < a && ((b += String.fromCharCode((((a -= 65536) >>> 10) & 1023) | 55296)), (a = 1023 & (56320 | a))), b + String.fromCharCode(a); + }) + .join(''); + } +} + +const parseAttr = (description) => { + const { attributes, text } = description; + const attrs = [...attributes]; + attrs.sort((b, d) => { + const f = b.start + b.length, + e = d.start + d.length; + return f !== e ? f - e : text_tag[b.detailData.style] - text_tag[d.detailData.style]; + }); + const n = []; + for (const q of attrs) { + const p = new TreeNode(q); + for (let w = q.start; 0 < n.length && n[n.length - 1].attr.start >= w; ) { + p.children.push(n.pop()); + } + p.children.reverse(); + n.push(p); + } + + const m = new Ucs2Text(text); + const renderSingle = (node) => { + const style = node?.attr?.detailData?.style; + switch (style) { + case 'BOLD': + return `${node.text}`; + case 'LINE_BREAK': + return `
`; + case 'LIST_ITEM': + return `
  • ${node.text}
  • `; + case 'LIST': + return ``; + case 'INLINE_CODE': + return `${node.text}`; + case 'LEGACY_PUBLISHING_EMPHASIS': + return `${node.text}`; + case 'STRIKETHROUGH': + return `${node.text}`; + case 'SUBSCRIPT': + return `${node.text}`; + case 'SUPERSCRIPT': + return `${node.text}`; + case 'UNDERLINE': + return `${node.text}`; + case 'ITALIC': + return `${node.text}`; + case 'PARAGRAPH': + return `

    ${node.text}

    `; + default: + return node.text || node; + } + }; + + const render = (node) => { + // DFS render + if (node.children.length === 0) { + node.text = m.slice(node.start, node.end); + return renderSingle(node); + } + const res = []; + let s = node.start; + for (const child of node.children) { + if (s < child.start) { + res.push(m.slice(s, child.start)); // added text before 1st child node + } + s = child.end; + res.push(render(child)); + } + + if (s < node.end) { + res.push(m.slice(s, node.end)); // added text after the last child node + } + node.text = res.join(''); + return renderSingle(node); + }; + + const q = []; + let p = 0; + for (let w = 0; w < n.length; w++) { + // BFS render + const e = n[w]; + if (e.start > p) { + q.push(m.slice(p, e.start)); + } + p = e.end; + q.push(render(e)); + } + if (p < m.length) { + q.push(m.slice(p, m.length)); + } + return q.join(''); +}; + +module.exports = { + parseAttr, +}; diff --git a/lib/v2/linkedin/cn/utils.js b/lib/v2/linkedin/cn/utils.js index ec7f148cf..6852f8770 100644 --- a/lib/v2/linkedin/cn/utils.js +++ b/lib/v2/linkedin/cn/utils.js @@ -2,6 +2,7 @@ const crypto = require('crypto'); const path = require('path'); const { art } = require('@/utils/render'); const got = require('@/utils/got'); +const { parseAttr } = require('./renderer'); const apiUrl = 'https://www.linkedin.cn/karpos/api/graphql'; const searchHitQueryId = 'searchSearchHitsByJob.be362cd720abd0ebf89b4bbc3253047f'; @@ -90,27 +91,29 @@ const parseSearchHit = async (ctx) => { }; }; -const parseJobPosting = async (jobPosting) => { +const parseJobPosting = (ctx, jobPosting) => { const entityUrn = jobPosting.entityUrn; - const variables = { - jobPostingUrn: entityUrn, - }; - const resp = await got.post(apiUrl, { - headers: makeHeader(), - body: makeBody({ - operationName: 'jobViewPage', - variables: makeVariables(variables), - queryId: jobPostingQueryId, - }), + return ctx.cache.tryGet(`linkedincn:${entityUrn}`, async () => { + const resp = await got.post(apiUrl, { + headers: makeHeader(), + body: makeBody({ + operationName: 'jobViewPage', + variables: makeVariables({ + jobPostingUrn: entityUrn, + }), + queryId: jobPostingQueryId, + }), + }); + const job = resp.data.data.jobsJobPostingsById; + job.desc = parseAttr(job.description); + return { + title: `${jobPosting.companyName} 正在找 ${jobPosting.title}`, + link: `https://www.linkedin.cn/incareer/jobs/view/${entityUrn.split(':').pop()}`, + guid: `linkedincn:${entityUrn}`, + description: art(path.join(__dirname, '../templates/cn/posting.art'), job), + pubDate: jobPosting.listedAt, + }; }); - const job = resp.data.data.jobsJobPostingsById; - return { - title: `${jobPosting.companyName} 正在找 ${jobPosting.title}`, - link: `https://www.linkedin.cn/incareer/jobs/view/${entityUrn.split(':').pop()}`, - guid: `linkedincn:${entityUrn}`, - description: art(path.join(__dirname, '../templates/cn/posting.art'), job), - pubDate: jobPosting.listedAt, - }; }; module.exports = { diff --git a/lib/v2/linkedin/templates/cn/posting.art b/lib/v2/linkedin/templates/cn/posting.art index e406bfa99..441113989 100644 --- a/lib/v2/linkedin/templates/cn/posting.art +++ b/lib/v2/linkedin/templates/cn/posting.art @@ -19,7 +19,7 @@

    公司介绍

    {{company.name}}

    员工人数:{{company.employeeCount}}

    -

    {{company.localizedDescription}}

    +

    {{company.localizedDescription}}

    {{ /if }}

    职位介绍

    -

    {{description.text}}

    +
    {{@ desc}}