From a644df0625d0ad8dd2d340e1f1e0796e0a58930a Mon Sep 17 00:00:00 2001 From: Ayobami Adegoke Date: Thu, 15 Oct 2026 10:53:54 +0100 Subject: [PATCH] fix(jobdanmark-search): drop a malformed closing tag the parser left as text (#526) A live page wrote its Jobtype item as "Fuldtid ". node-html-parser consumes well-formed markup but does not recognise a closing tag with a space after the "<" or the slash, so the fragment became a text node and overviewValue kept it: employmentType: ["Fuldtid "], matching none of the documented job-type vocabulary. The same cleanText feeds every text field on both branches. Strip only the closing-tag shape in cleanText before collapsing whitespace. The parser decodes entities before cleanText runs, so author text the page encoded as <div> or <T> arrives as a literal
/ and must stay; a probe of what the parser leaks shows only malformed closing shapes (, , < /li>). Two fixtures: the stray tag in both the Jobtype item and a description paragraph (fails on master), and an entity-encoded
that survives. Observed by the maintainer in the #516 merge note. --- .../cli/src/commands/detail.ts | 12 ++++- .../cli/tests/detail-parsing.test.ts | 44 +++++++++++++++++++ CHANGELOG.md | 13 ++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/.agents/skills/jobdanmark-search/cli/src/commands/detail.ts b/.agents/skills/jobdanmark-search/cli/src/commands/detail.ts index 88183365..406b4bbc 100644 --- a/.agents/skills/jobdanmark-search/cli/src/commands/detail.ts +++ b/.agents/skills/jobdanmark-search/cli/src/commands/detail.ts @@ -52,8 +52,18 @@ interface DetailResult { applyUrl: string | null } +// A malformed closing tag that reached the text layer. node-html-parser +// consumes well-formed markup (``, `
`, ``) but does not +// recognise a closing tag with a space after the `<` or the slash, so `` +// survives as text; a live jobdanmark page emitted +// `employmentType: ["Fuldtid "]` that way. Only the closing shape is +// stripped: the parser decodes entities before this runs, so author text the +// page encoded as `<div>` or `<T>` arrives here as a literal +// `
` / `` and must stay. +const STRAY_CLOSING_TAG = /<\s*\/\s*[a-z][a-z0-9-]*\s*>/gi + function cleanText(text: string): string { - return text.replace(/\s+/g, " ").trim() + return text.replace(STRAY_CLOSING_TAG, " ").replace(/\s+/g, " ").trim() } /** diff --git a/.agents/skills/jobdanmark-search/cli/tests/detail-parsing.test.ts b/.agents/skills/jobdanmark-search/cli/tests/detail-parsing.test.ts index 8c1fa258..41006671 100644 --- a/.agents/skills/jobdanmark-search/cli/tests/detail-parsing.test.ts +++ b/.agents/skills/jobdanmark-search/cli/tests/detail-parsing.test.ts @@ -65,6 +65,50 @@ describe("parseJobPostingFromHtml", () => { expect(parsed.validThrough).toBeNull(); }); + test("drops a malformed closing tag that the parser left as text (#525)", () => { + // A live page (konsulent-til-renoveringsprogram) wrote its Jobtype item as + // `Fuldtid `; node-html-parser does not recognise `` as a tag, + // so it reached employmentType verbatim. The same cleanText feeds the title, + // company, workplace and description on this branch. + const html = HTML_WITHOUT_JSON_LD.replace( + "
  • Jobtype: Fuldtid
  • ", + "
  • Jobtype: Fuldtid
  • ", + ).replace( + "

    Hvad nu hvis du med få klik kunne undersøge dit lokalområde?

    ", + "

    Hvad nu hvis du med få klik kunne undersøge dit lokalområde?

    ", + ); + + const parsed = parseJobPostingFromHtml( + html, + "journalistisk-udvikler", + "https://jobdanmark.dk/job/journalistisk-udvikler", + ); + + expect(parsed.employmentType).toEqual(["Fuldtid"]); + expect(parsed.description.split("\n")[0]).toBe("Hvad nu hvis du med få klik kunne undersøge dit lokalområde?"); + expect(JSON.stringify(parsed)).not.toMatch(/<\s*\/?\s*[a-z]+\s*>/i); + }); + + test("keeps author text the page entity-encoded as a literal tag (#525)", () => { + // node-html-parser decodes entities before cleanText runs, so `<div>` + // in a description arrives as a literal `
    `. Only the malformed + // closing-tag shape is stripped; an opening tag or a type parameter stays. + const html = HTML_WITHOUT_JSON_LD.replace( + "

    Hvad nu hvis du med få klik kunne undersøge dit lokalområde?

    ", + "

    Erfaring med <div> og <span> i HTML, og <T> generics; x < y > z.

    ", + ); + + const parsed = parseJobPostingFromHtml( + html, + "journalistisk-udvikler", + "https://jobdanmark.dk/job/journalistisk-udvikler", + ); + + expect(parsed.description.split("\n")[0]).toBe( + "Erfaring med
    og i HTML, og generics; x < y > z.", + ); + }); + test("does not reject titles containing '404' mid-phrase", () => { const htmlWith404InTitle = HTML_WITHOUT_JSON_LD.replace( "Journalistisk udvikler søges | jobdanmark", diff --git a/CHANGELOG.md b/CHANGELOG.md index 648dc484..848e3c4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,19 @@ per-file diff commands. ### Fixed +- **`jobdanmark-search detail` no longer keeps a malformed closing tag as text on the + rendered-HTML branch** (#525, `detail.ts`, `tests/detail-parsing.test.ts`) - a live page wrote + its Jobtype item as `Fuldtid `, and because `node-html-parser` does not recognise a + closing tag with a space after the slash, the fragment reached the output verbatim: + `employmentType: ["Fuldtid "]`, a value that matches none of the documented job-type + vocabulary. The same `cleanText` feeds every text field on both branches, so any of them + could carry it. `cleanText` now drops the closing-tag shape (``, ``, `< /li>`) + before collapsing whitespace - and only that shape, because the parser decodes entities + before `cleanText` runs, so author text the page encoded as `<div>` or `<T>` + arrives as a literal `
    ` / `` and must survive. Two fixtures: the stray tag in both + the Jobtype item and a description paragraph (fails on master), and an entity-encoded + `
    ` in a description that stays in the text. + - **`/add-portal` includes the `.agents` prefix in generated `allowed-tools`** (#545, `.claude/commands/add-portal.md`, `tests/test_add_portal_command.py`) - the specification template for the new portal's `SKILL.md` frontmatter wrote `allowed-tools: Bash(bun run skills//cli/src/cli.ts *)`