// The clipper's converter (shared/htmlToMarkdown.ts): the dozen tags an // article is made of come through as Markdown, everything that is not // content is dropped, and prose that happens to contain Markdown's own marks // stays prose. import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { decodeEntities, htmlTitle, htmlToMarkdown } from "../shared/htmlToMarkdown.ts"; const md = (html: string, baseUrl?: string): string => htmlToMarkdown(html, { baseUrl }); describe("htmlToMarkdown", () => { it("headings, paragraphs and rules", () => { assert.equal( md("

Title

One.

Two\n three.


Sub head

"), "# Title\n\nOne.\n\nTwo three.\n\n---\n\n### Sub *head*", ); }); it("bold, italic, code, strikethrough — with the page's own spacing kept outside the marks", () => { assert.equal(md("

a bold b it x `y` z gone

"), "a **bold** b *it* `` x `y` z `` ~~gone~~"); assert.equal(md("

empty

"), "empty"); }); it("links resolve against the page, and only to somewhere a reader can go", () => { assert.equal(md('rel', "https://ex.org/dir/page"), "[rel](https://ex.org/x?a=1)"); assert.equal(md('js'), "js"); assert.equal(md('pic', "https://ex.org/p"), "[![pic](https://ex.org/i.png)](https://ex.org/)"); assert.equal(md(''), "[https://ex.org/](https://ex.org/)"); // A outranks the caller's base. assert.equal(md('q', "https://a.org/"), "[q](https://b.org/q)"); }); it("images by URL, never inlined", () => { assert.equal(md('A [pic]', "https://ex.org/a/"), "![A pic](https://ex.org/a/pic.jpg)"); assert.equal(md('x'), ""); }); it("lists nest, number, and keep their start", () => { assert.equal(md(""), "- a\n- b\n - c"); assert.equal(md('
  1. x
  2. y
'), "3. x\n4. y"); // Unclosed
  • , as pages actually write them. assert.equal(md(""), "- a\n- b"); // A loose item keeps its paragraphs, hung under the marker. assert.equal(md("
    1. one

      two

    "), "1. one\n\n two"); }); it("block quotes and fenced code", () => { assert.equal(md("

    q1

    q2

    "), "> q1\n>\n> q2"); assert.equal(md('
    const a = 1;\n  if (a < 2) {}
    '), "```ts\nconst a = 1;\n if (a < 2) {}\n```"); // Backticks inside the code lengthen the fence. assert.equal(md("
    ```\nx\n```
    "), "````\n```\nx\n```\n````"); }); it("the simplest tables", () => { assert.equal( md("
    ab|c
    12
    "), "| a | b\\|c |\n| --- | --- |\n| 1 | 2 |", ); }); it("drops what is not content", () => { const page = "T" + "

    Real.

    no" + "
    "; assert.equal(md(page), "Real."); assert.equal(md("

    a

    "), "a"); }); it("prefers
    , then
    , then the body; a fragment is itself", () => { assert.equal(md("

    art

    "), "art"); assert.equal(md("

    whole

    "), "whole"); assert.equal(md("
    frag

    ment

    "), "frag\n\nment"); }); it("escapes Markdown's own marks in prose, but not mid-word underscores", () => { assert.equal(md("

    2 * 3 [cite] snake_case _lead trail_ a<b

    "), "2 \\* 3 \\[cite\\] snake_case \\_lead trail\\_ a\\# not a heading

    > not a quote

    1. not a list

    - nor this

    "), "\\# not a heading\n\n\\> not a quote\n\n1\\. not a list\n\n\\- nor this"); assert.equal(md("

    a > b and #tag stay

    "), "a > b and #tag stay"); }); it("decodes entities, keeps unknown ones visible, and honours
    ", () => { assert.equal(decodeEntities("&AB— &bogus;"), "&AB\u2014\u00a0&bogus;"); assert.equal(md("

    a
    b

    "), "a \nb"); }); it("names the page", () => { assert.equal(htmlTitle(" A &\n B "), "A & B"); assert.equal(htmlTitle("

    none

    "), null); }); it("closes an unclosed row when the next one opens, so every row of an old-style table survives", () => { assert.equal( htmlToMarkdown("
    ab
    cd
    "), "| a | b |\n| --- | --- |\n| c | d |", ); assert.equal(htmlToMarkdown("
    • one
      • inner
      • inner2
    • two
    "), "- one\n - inner\n - inner2\n- two"); }); it("caps the depth a crafted page can nest, and keeps the words", () => { const deep = `${"
    ".repeat(50_000)}deep${"
    ".repeat(50_000)}`; assert.equal(htmlToMarkdown(deep), "deep"); const open = `${"".repeat(50_000)}still here`; assert.equal(htmlToMarkdown(open), "still here"); }); it("never throws on rubbish", () => { for (const junk of ["<", "<<>>", "

    ", "