Token Classification
Transformers
ONNX
Safetensors
multilingual
bert
autofill
password-manager
form-classification
field-classification
browser-extension
Instructions to use ProCreations/Zap with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ProCreations/Zap with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="ProCreations/Zap")# Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("ProCreations/Zap") model = AutoModelForTokenClassification.from_pretrained("ProCreations/Zap", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download zap-extract.js from ProCreations/Zap: direct link, hf CLI and curl.
- Browser
- Download file 13.4 kB
-
https://huggingface.co/ProCreations/Zap/resolve/main/zap-extract.js
- Command line
-
hf download hf://ProCreations/Zap/zap-extract.js
-
curl -L -o zap-extract.js https://huggingface.co/ProCreations/Zap/resolve/main/zap-extract.js
13.4 kB
| // Zap form extractor — JavaScript port of zap/extract.py (keep the two in lockstep). | |
| // Runs on a live DOM (browser extension content script) or jsdom. | |
| // const forms = zapExtract(document, location.href); | |
| // forms[i] = { root, elements, ctx, fields, text } // `text` is what the Zap model reads | |
| (function (global) { | |
| "use strict"; | |
| const SKIP_INPUT_TYPES = new Set(["hidden", "submit", "button", "reset", "image", "file", "checkbox", "radio", "range", "color"]); | |
| const FIELD_TAGS = new Set(["input", "select", "textarea"]); | |
| const NO_TEXT_TAGS = new Set(["script", "style", "noscript", "template", "svg", "select", "textarea", "option", "head"]); | |
| const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6", "legend"]); | |
| const DATA_ATTRS = ["data-testid", "data-test", "data-qa", "data-cy", "formcontrolname", "data-field", "data-name", "data-type"]; | |
| const MAX_FIELDS = 40; | |
| const MAX_FORMS = 10; | |
| const CTX_LIMITS = { title: 60, url: 60, form: 60, head: 60, btn: 60, link: 80, txt: 120, hid: 40 }; | |
| const FIELD_LIMITS = { name: 40, id: 40, class: 30, ac: 30, ph: 50, lbl: 60, aria: 50, tip: 40, near: 50, after: 30, mode: 12, max: 6, opt: 60, data: 30 }; | |
| const CTX_ORDER = [["title", "[TITLE]"], ["url", "[URL]"], ["form", "[FORM]"], ["head", "[HEAD]"], ["btn", "[BTN]"], ["link", "[LINK]"], ["txt", "[TXT]"], ["hid", "[HID]"]]; | |
| const FIELD_ORDER = [["type", "[TYPE]"], ["name", "[NAME]"], ["id", "[ID]"], ["class", "[CLASS]"], ["ac", "[AC]"], ["ph", "[PH]"], ["lbl", "[LBL]"], ["aria", "[ARIA]"], ["tip", "[TIP]"], ["near", "[NEAR]"], ["after", "[AFTER]"], ["mode", "[MODE]"], ["max", "[MAX]"], ["req", "[REQ]"], ["opt", "[OPT]"], ["data", "[DATA]"]]; | |
| const ELEMENT = 1, TEXT = 3, CDATA = 4; | |
| function norm(s) { | |
| if (!s) return ""; | |
| return s.normalize("NFKC").replace(/\s+/g, " ").trim(); | |
| } | |
| function cut(s, n) { | |
| const cps = Array.from(s); | |
| return cps.length <= n ? s.trimEnd() : cps.slice(0, n).join("").trimEnd(); | |
| } | |
| function words(s) { | |
| if (!s) return ""; | |
| s = s.normalize("NFKC"); | |
| s = s.replace(/([a-z])([A-Z])/g, "$1 $2"); | |
| s = s.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2"); | |
| s = s.replace(/([A-Za-z])([0-9])/g, "$1 $2"); | |
| s = s.replace(/([0-9])([A-Za-z])/g, "$1 $2"); | |
| s = s.replace(/[^\p{L}\p{N}\p{M}]/gu, " "); | |
| return s.replace(/\s+/g, " ").trim().toLowerCase(); | |
| } | |
| function tag(el) { return el && el.nodeType === ELEMENT ? el.localName.toLowerCase() : ""; } | |
| function attr(el, name) { const v = el.getAttribute(name); return v === null ? "" : v; } | |
| function has(el, name) { return el.hasAttribute(name); } | |
| function unquote(p) { try { return decodeURIComponent(p); } catch (e) { return p; } } | |
| function text(el) { | |
| const parts = []; | |
| (function walk(e) { | |
| if (NO_TEXT_TAGS.has(tag(e))) return; | |
| for (let c = e.firstChild; c; c = c.nextSibling) { | |
| if (c.nodeType === ELEMENT) walk(c); | |
| else if (c.nodeType === TEXT || c.nodeType === CDATA) parts.push(c.data); | |
| } | |
| })(el); | |
| return norm(parts.join(" ")); | |
| } | |
| function inputType(el) { | |
| const t = tag(el); | |
| if (t === "input") return attr(el, "type").trim().toLowerCase() || "text"; | |
| return t; | |
| } | |
| function hiddenAttr(el) { | |
| if (has(el, "hidden") || attr(el, "aria-hidden").trim().toLowerCase() === "true") return true; | |
| const style = attr(el, "style").replace(/ /g, "").toLowerCase(); | |
| return style.includes("display:none") || style.includes("visibility:hidden"); | |
| } | |
| function isField(el) { | |
| const t = tag(el); | |
| if (!FIELD_TAGS.has(t)) return false; | |
| if (t === "input" && SKIP_INPUT_TYPES.has(inputType(el))) return false; | |
| return !has(el, "disabled"); | |
| } | |
| function visibleWithin(el, root) { | |
| for (let n = el; n; n = n.parentElement) { | |
| if (hiddenAttr(n)) return false; | |
| if (n === root) return true; | |
| } | |
| return true; | |
| } | |
| function containsField(el) { | |
| return FIELD_TAGS.has(tag(el)) || !!el.querySelector("input,select,textarea"); | |
| } | |
| // Previous / next siblings in DOM order, nearest first: ["t", string] or ["e", element] | |
| function* prevNodes(el) { | |
| for (let n = el.previousSibling; n; n = n.previousSibling) { | |
| if (n.nodeType === TEXT || n.nodeType === CDATA) yield ["t", n.data]; | |
| else if (n.nodeType === ELEMENT) yield ["e", n]; | |
| } | |
| } | |
| function* nextNodes(el) { | |
| for (let n = el.nextSibling; n; n = n.nextSibling) { | |
| if (n.nodeType === TEXT || n.nodeType === CDATA) yield ["t", n.data]; | |
| else if (n.nodeType === ELEMENT) yield ["e", n]; | |
| } | |
| } | |
| function nearText(el, root) { | |
| let node = el; | |
| for (let level = 0; level < 3; level++) { | |
| let seen = 0; | |
| for (const [kind, v] of prevNodes(node)) { | |
| let t; | |
| if (kind === "t") t = norm(v); | |
| else { if (containsField(v)) return ""; t = text(v); } | |
| if (t) return t; | |
| if (kind === "e") { seen++; if (seen >= 3) break; } | |
| } | |
| if (node === root) break; | |
| node = node.parentElement; | |
| if (!node || node === root) break; | |
| } | |
| return ""; | |
| } | |
| function afterText(el) { | |
| let seen = 0; | |
| for (const [kind, v] of nextNodes(el)) { | |
| let t; | |
| if (kind === "t") t = norm(v); | |
| else { if (containsField(v)) return ""; t = text(v); } | |
| if (t) return t; | |
| if (kind === "e") { seen++; if (seen >= 2) break; } | |
| } | |
| return ""; | |
| } | |
| function makePage(doc, url) { | |
| const byId = new Map(), labelFor = new Map(); | |
| for (const e of doc.getElementsByTagName("*")) { | |
| const i = e.getAttribute("id"); | |
| if (i && !byId.has(i)) byId.set(i, e); | |
| if (tag(e) === "label" && e.getAttribute("for")) { | |
| const f = e.getAttribute("for"); | |
| if (!labelFor.has(f)) labelFor.set(f, []); | |
| labelFor.get(f).push(e); | |
| } | |
| } | |
| const t = doc.querySelector("title"); | |
| return { doc, url, byId, labelFor, title: t ? norm(t.textContent) : "" }; | |
| } | |
| function labelText(el, page) { | |
| const out = []; | |
| for (const ref of attr(el, "aria-labelledby").split(/\s+/).filter(Boolean)) { | |
| const e = page.byId.get(ref); | |
| if (e) out.push(text(e)); | |
| } | |
| const i = el.getAttribute("id"); | |
| if (i) for (const lab of page.labelFor.get(i) || []) out.push(text(lab)); | |
| for (let n = el.parentElement; n; n = n.parentElement) { | |
| if (tag(n) === "label") { out.push(text(n)); break; } | |
| } | |
| const uniq = []; | |
| for (const t of out) if (t && !uniq.includes(t)) uniq.push(t); | |
| return uniq.join(" | "); | |
| } | |
| function isButtonish(e) { | |
| const t = tag(e); | |
| if (t === "button") return true; | |
| if (t === "input" && ["submit", "button", "image"].includes(inputType(e))) return true; | |
| return attr(e, "role").toLowerCase() === "button"; | |
| } | |
| function isHeading(e) { return HEADING_TAGS.has(tag(e)) || attr(e, "role").toLowerCase() === "heading"; } | |
| function* iterSelf(root) { yield root; yield* root.getElementsByTagName("*"); } | |
| function heading(root) { | |
| for (const e of iterSelf(root)) { | |
| if (isHeading(e)) { const t = text(e); if (t) return t; } | |
| } | |
| let node = root; | |
| for (let level = 0; level < 4; level++) { | |
| let n = 0; | |
| for (const [kind, sib] of prevNodes(node)) { | |
| if (kind !== "e") continue; | |
| n++; | |
| if (n > 5) break; | |
| if (isHeading(sib)) { const t = text(sib); if (t) return t; } | |
| const hs = Array.from(sib.getElementsByTagName("*")).filter(isHeading); | |
| for (let k = hs.length - 1; k >= 0; k--) { const t = text(hs[k]); if (t) return t; } | |
| } | |
| node = node.parentElement; | |
| if (!node || tag(node) === "body" || tag(node) === "html") break; | |
| } | |
| return ""; | |
| } | |
| function buttonTexts(root) { | |
| const out = []; | |
| for (const e of iterSelf(root)) { | |
| if (!isButtonish(e)) continue; | |
| const t = text(e) || norm(attr(e, "value")) || norm(attr(e, "aria-label")) || norm(attr(e, "alt")) || norm(attr(e, "title")); | |
| if (t && !out.includes(t)) out.push(cut(t, 30)); | |
| if (out.length >= 3) break; | |
| } | |
| return out.join(" | "); | |
| } | |
| function linkTexts(root) { | |
| const out = []; | |
| for (const e of iterSelf(root)) { | |
| if (tag(e) !== "a") continue; | |
| const t = text(e) || norm(attr(e, "aria-label")) || norm(attr(e, "title")); | |
| if (t && !out.includes(t)) out.push(cut(t, 30)); | |
| if (out.length >= 4) break; | |
| } | |
| return out.join(" | "); | |
| } | |
| function hiddenNames(root) { | |
| const out = []; | |
| for (const e of iterSelf(root)) { | |
| if (tag(e) === "input" && inputType(e) === "hidden") { | |
| const w = words(attr(e, "name")); | |
| if (w && !out.includes(w)) out.push(w); | |
| if (out.length >= 4) break; | |
| } | |
| } | |
| return out.join(" "); | |
| } | |
| function urlPath(u, base) { | |
| try { const x = base ? new URL(u, base) : new URL(u); return { host: x.hostname, path: x.pathname }; } catch (e) { return null; } | |
| } | |
| function urlWords(url) { | |
| const u = urlPath(url); | |
| if (!u) return ""; | |
| const host = u.host.split("."); | |
| const lead = host.length >= 3 && host[0] !== "www" ? host[0] : ""; | |
| return words(lead + " " + unquote(u.path)); | |
| } | |
| function formWords(root) { | |
| const parts = [attr(root, "id"), attr(root, "name"), attr(root, "class")]; | |
| if (tag(root) === "form") { | |
| const a = attr(root, "action"); | |
| const u = a ? urlPath(a, "http://x/") : null; | |
| parts.push(u ? unquote(u.path) : ""); | |
| } else parts.push(attr(root, "role")); | |
| return (words(parts.join(" ")) + " " + norm(attr(root, "aria-label")).toLowerCase()).trim(); | |
| } | |
| function fieldFeatures(el, root, page) { | |
| const t = tag(el); | |
| const f = { type: t !== "input" ? t : "input " + inputType(el) }; | |
| f.name = words(attr(el, "name")); | |
| f.id = words(attr(el, "id")); | |
| f.class = words(attr(el, "class")); | |
| f.ac = norm(attr(el, "autocomplete")).toLowerCase(); | |
| f.ph = norm(attr(el, "placeholder")); | |
| f.lbl = labelText(el, page); | |
| f.aria = norm(attr(el, "aria-label")); | |
| f.tip = norm(attr(el, "title")); | |
| const near = nearText(el, root); | |
| f.near = near === f.lbl || near === f.ph || near === f.aria ? "" : near; | |
| f.after = afterText(el); | |
| f.mode = norm(attr(el, "inputmode")).toLowerCase(); | |
| f.max = norm(attr(el, "maxlength")); | |
| f.req = has(el, "required") || attr(el, "aria-required").toLowerCase() === "true"; | |
| if (t === "select") { | |
| const opts = Array.from(el.getElementsByTagName("option")).map((o) => norm(o.textContent)).filter(Boolean); | |
| f.opt = opts.length ? opts.length + ": " + opts.slice(0, 6).join(" | ") : ""; | |
| } else f.opt = ""; | |
| const data = DATA_ATTRS.filter((a) => attr(el, a).trim()).map((a) => norm(attr(el, a))); | |
| f.data = words(data.slice(0, 2).join(" ")); | |
| for (const [k, n] of Object.entries(FIELD_LIMITS)) if (typeof f[k] === "string") f[k] = cut(f[k], n); | |
| return f; | |
| } | |
| function formContext(root, page) { | |
| const c = { title: page.title, url: urlWords(page.url), form: formWords(root), head: heading(root), | |
| btn: buttonTexts(root), link: linkTexts(root), txt: text(root), hid: hiddenNames(root) }; | |
| for (const k of Object.keys(c)) c[k] = cut(c[k], CTX_LIMITS[k]); | |
| return c; | |
| } | |
| function groupRoot(el, body) { | |
| let node = el.parentElement; | |
| for (let level = 0; level < 6; level++) { | |
| if (!node || node === body || tag(node) === "body" || tag(node) === "html") break; | |
| for (const d of iterSelf(node)) { | |
| if (isButtonish(d) && !d.closest("form")) return node; | |
| } | |
| node = node.parentElement; | |
| } | |
| node = el; | |
| for (let level = 0; level < 3; level++) { | |
| const p = node.parentElement; | |
| if (!p || p === body || tag(p) === "body" || tag(p) === "html") break; | |
| node = p; | |
| } | |
| return node; | |
| } | |
| function findForms(doc) { | |
| const body = doc.body || doc.documentElement; | |
| const groups = new Map(); | |
| for (const el of body.getElementsByTagName("*")) { | |
| if (!isField(el)) continue; | |
| let root = el.parentElement ? el.parentElement.closest("form") : null; | |
| if (!root) root = groupRoot(el, body); | |
| if (!visibleWithin(el, root)) continue; | |
| if (!groups.has(root)) groups.set(root, []); | |
| const g = groups.get(root); | |
| if (g.length < MAX_FIELDS) g.push(el); | |
| } | |
| return Array.from(groups.entries()).slice(0, MAX_FORMS); | |
| } | |
| function serializeCtx(ctx) { | |
| return CTX_ORDER.filter(([k]) => ctx[k]).map(([k, key]) => key + " " + ctx[k]).join(" "); | |
| } | |
| function serializeField(f) { | |
| const out = ["[FLD]"]; | |
| for (const [k, key] of FIELD_ORDER) { | |
| const v = f[k]; | |
| if (k === "req") { if (v) out.push("[REQ]"); } else if (v) out.push(key + " " + v); | |
| } | |
| return out.join(" "); | |
| } | |
| function serialize(ctx, fields) { | |
| return [serializeCtx(ctx)].concat(fields.map(serializeField)).join(" "); | |
| } | |
| function zapExtract(doc, url) { | |
| const page = makePage(doc, url); | |
| return findForms(doc).map(([root, elements]) => { | |
| const fields = elements.map((el) => fieldFeatures(el, root, page)); | |
| const ctx = formContext(root, page); | |
| return { kind: tag(root) === "form" ? "form" : "group", root, elements, ctx, fields, text: serialize(ctx, fields) }; | |
| }); | |
| } | |
| const api = { zapExtract, serialize, serializeCtx, serializeField, words, norm }; | |
| if (typeof module !== "undefined" && module.exports) module.exports = api; | |
| else global.Zap = Object.assign(global.Zap || {}, api); | |
| })(typeof globalThis !== "undefined" ? globalThis : this); | |