@gafj/gafj 0.1.19 → 0.1.20
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/http/guard.js +1 -1
- package/package.json +1 -1
- package/ui/lib.js +35 -0
- package/ui/screens/postings.js +7 -15
- package/ui/screens/welcome.js +6 -7
package/http/guard.js
CHANGED
|
@@ -91,7 +91,7 @@ function readJson(req, cap = BODY_CAP) {
|
|
|
91
91
|
const chunks = [];
|
|
92
92
|
req.on("data", (c) => {
|
|
93
93
|
size += c.length;
|
|
94
|
-
if (size > cap) { reject(Object.assign(new Error(
|
|
94
|
+
if (size > cap) { req.removeAllListeners("data"); req.resume(); reject(Object.assign(new Error(`body larger than ${Math.round(cap / 1024 / 1024)} MB; paste less`), { status: 413 })); return; }
|
|
95
95
|
chunks.push(c);
|
|
96
96
|
});
|
|
97
97
|
req.on("end", () => {
|
package/package.json
CHANGED
package/ui/lib.js
CHANGED
|
@@ -90,3 +90,38 @@ export function useInstall() {
|
|
|
90
90
|
const standalone = matchMedia("(display-mode: standalone)").matches || navigator.standalone === true;
|
|
91
91
|
return { canInstall: !!installer.event, installed: installer.installed || standalone, install: async () => { if (!installer.event) return; installer.event.prompt(); await installer.event.userChoice; installer.event = null; } };
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A job posting as pasted: markdown links become their text, bare URLs and tracking lines go, blank runs
|
|
96
|
+
* collapse. A LinkedIn copy carries a wall of overlay URLs that says nothing about the job.
|
|
97
|
+
*/
|
|
98
|
+
export function cleanPosting(text) {
|
|
99
|
+
return String(text || "")
|
|
100
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, "")
|
|
101
|
+
.replace(/\[([^\]]*)\]\((?:https?:)?\/\/[^)\s]*(?:\s+"[^"]*")?\)/g, "$1")
|
|
102
|
+
.replace(/\((?:https?:)?\/\/[^)\s]{20,}\)/g, "")
|
|
103
|
+
.split(/\r?\n/)
|
|
104
|
+
.map((l) => l.replace(/\s+$/, ""))
|
|
105
|
+
.filter((l, i, all) => !/^\s*(?:https?:)?\/\/\S+\s*$/.test(l) || (/\/(jobs?|careers?|positions?|openings?)\//i.test(l) && all.findIndex((x) => /^\s*(?:https?:)?\/\/\S+\s*$/.test(x) && /\/(jobs?|careers?|positions?|openings?)\//i.test(x)) === i))
|
|
106
|
+
.join("\n")
|
|
107
|
+
.replace(/[ \t]{3,}/g, " ")
|
|
108
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
109
|
+
.trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const NOISE = /\b(ago|hours?|days?|weeks?|applicants?|promoted|easy apply|reposted|save|share|show more|about the job|job details|united states|remote|hybrid|on-site|full-time|part-time|contract|tailor my resume|help me stand out|create cover letter)\b/i;
|
|
113
|
+
const TITLE = /\b(director|manager|engineer|analyst|lead|head|vp|vice president|specialist|coordinator|supervisor|associate|consultant|architect|designer|developer|scientist|officer|president|chief|planner|buyer|controller|administrator|technician|representative|recruiter|nurse|teacher|intern|accountant|assistant|generalist|partner|strategist|writer|editor)\b/i;
|
|
114
|
+
/** Title and company guessed from a posting's first lines; both are the person's to correct. */
|
|
115
|
+
export function guessPosting(text) {
|
|
116
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(0, 20);
|
|
117
|
+
const plain = (l) => l.length <= 90 && !/^https?:/i.test(l) && !/^(at|company|location|posted|apply|share)\b/i.test(l) && !NOISE.test(l);
|
|
118
|
+
const title = lines.find((l) => plain(l) && TITLE.test(l) && !/[.:;]$/.test(l) && !/\b(company|inc|llc|corp|group|ltd)\.?$/i.test(l)) || lines.find(plain) || "";
|
|
119
|
+
let company = "";
|
|
120
|
+
for (const l of lines) { const m = l.match(/^(?:at|company[:\s]+|employer[:\s]+)\s*([^|·,]{2,60})/i); if (m) { company = m[1].trim(); break; } }
|
|
121
|
+
const i = lines.findIndex((l) => l === title);
|
|
122
|
+
const nameish = (l) => { const c = l.split(/\s[·|]\s/)[0].replace(/^[·|\-\s]+/, "").trim(); return c && c !== title && c.length <= 60 && !/^https?:/i.test(c) && !/\d{4}/.test(c) && !NOISE.test(c) && !/[.:;]$/.test(c) && c.split(/\s+/).length <= 8 ? c : ""; };
|
|
123
|
+
// the line after the title ("Company · City"), else a short name-like line above it, else one soon after
|
|
124
|
+
if (!company) company = [lines[i + 1] || ""].map(nameish).find(Boolean) || lines.slice(0, i).map(nameish).find(Boolean) || lines.slice(i + 2, i + 6).map(nameish).find(Boolean) || "";
|
|
125
|
+
const url = lines.find((l) => /^https?:\/\//i.test(l)) || "";
|
|
126
|
+
return { title, company, url };
|
|
127
|
+
}
|
package/ui/screens/postings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { html, useState, useEffect } from "../vendor/preact.mjs";
|
|
2
|
-
import { api } from "../lib.js";
|
|
2
|
+
import { cleanPosting, guessPosting, api } from "../lib.js";
|
|
3
3
|
|
|
4
4
|
/** Screen 2: paste a posting, store and scan locally, edit the proposed categories, freeze, log the application. */
|
|
5
5
|
|
|
@@ -39,21 +39,13 @@ export function Postings({ live, id }) {
|
|
|
39
39
|
const [form, setForm] = useState({ company: "", title: "", source_url: "", location: "", comp_text: "", raw_text: "" });
|
|
40
40
|
const [guessed, setGuessed] = useState({ company: true, title: true });
|
|
41
41
|
// the paste is all a person should have to give; company and title are guessed from its first lines and can be corrected
|
|
42
|
-
const onPaste = (
|
|
43
|
-
const
|
|
42
|
+
const onPaste = (raw) => {
|
|
43
|
+
const text = cleanPosting(raw);
|
|
44
|
+
const g = guessPosting(text);
|
|
44
45
|
const next = { ...form, raw_text: text };
|
|
45
|
-
if (guessed.title)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
if (guessed.company) {
|
|
50
|
-
let c = "";
|
|
51
|
-
for (const l of lines) { const m = l.match(/^(?:at|company[:\s]+|employer[:\s]+)\s*([^|·,]{2,60})/i); if (m) { c = m[1].trim(); break; } }
|
|
52
|
-
if (!c) { const i = lines.findIndex((l) => l === next.title); const cand = lines[i + 1] || ""; if (cand && cand.length <= 50 && !/^https?:/i.test(cand) && !/\d{4}/.test(cand)) c = cand.replace(/^[·|\-\s]+/, ""); }
|
|
53
|
-
next.company = c;
|
|
54
|
-
}
|
|
55
|
-
const url = lines.find((l) => /^https?:\/\//i.test(l));
|
|
56
|
-
if (url && !form.source_url) next.source_url = url;
|
|
46
|
+
if (guessed.title) next.title = g.title;
|
|
47
|
+
if (guessed.company) next.company = g.company;
|
|
48
|
+
if (g.url && !form.source_url) next.source_url = g.url;
|
|
57
49
|
setForm(next);
|
|
58
50
|
};
|
|
59
51
|
const [err, setErr] = useState("");
|
package/ui/screens/welcome.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { html, useState, useEffect } from "../vendor/preact.mjs";
|
|
2
|
-
import { api } from "../lib.js";
|
|
2
|
+
import { api, cleanPosting, guessPosting } from "../lib.js";
|
|
3
3
|
import { Onboarding, DraftForm, fromDraft, Positions, ContactForm } from "./kb.js";
|
|
4
4
|
import { PacketModal } from "./packet.js";
|
|
5
5
|
|
|
@@ -64,13 +64,12 @@ function StepPosting({ reload }) {
|
|
|
64
64
|
const [title, setTitle] = useState("");
|
|
65
65
|
const [g, setG] = useState({ company: true, title: true });
|
|
66
66
|
const [err, setErr] = useState("");
|
|
67
|
-
const onPaste = (
|
|
67
|
+
const onPaste = (raw) => {
|
|
68
|
+
const t = cleanPosting(raw);
|
|
68
69
|
setText(t);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
if (g.
|
|
72
|
-
if (g.company) { co = ""; for (const l of lines) { const m = l.match(/^(?:at|company[:\s]+|employer[:\s]+)\s*([^|·,]{2,60})/i); if (m) { co = m[1].trim(); break; } } if (!co) { const i = lines.findIndex((l) => l === ti); const c = lines[i + 1] || ""; if (c && c.length <= 50 && !/^https?:/i.test(c) && !/\d{4}/.test(c)) co = c.replace(/^[·|\-\s]+/, ""); } }
|
|
73
|
-
setTitle(ti); setCompany(co);
|
|
70
|
+
const gu = guessPosting(t);
|
|
71
|
+
if (g.title) setTitle(gu.title);
|
|
72
|
+
if (g.company) setCompany(gu.company);
|
|
74
73
|
};
|
|
75
74
|
const store = async (e) => {
|
|
76
75
|
e.preventDefault(); setErr("");
|