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 'LEGACY_PUBLISHING_EMPHASIS':
+ return `${node.text}`;
+ case 'STRIKETHROUGH':
+ 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}}
+