{
  "path": "/3mf6xazbcj22u",
  "site": "at://did:plc:lvkhxfkdwqgwrpdek3h3q2gc/site.standard.publication/3m2ojl75sm22f",
  "tags": [
    "unison",
    "markdown",
    "parser",
    "html-parse"
  ],
  "$type": "site.standard.document",
  "title": "Parse HTML into Markdown in Unison",
  "content": {
    "$type": "pub.leaflet.content",
    "pages": [
      {
        "id": "019c7471-ed4f-7660-a0aa-fb9abde4b804",
        "$type": "pub.leaflet.pages.linearDocument",
        "blocks": [
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "This is a sequel to my earlier post on parsing HTML in Unison:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.blockquote",
              "facets": [
                {
                  "index": {
                    "byteEnd": 34,
                    "byteStart": 0
                  },
                  "features": [
                    {
                      "uri": "https://notes.kaushikc.org/3mdxmxpjkg22c",
                      "$type": "pub.leaflet.richtext.facet#link"
                    }
                  ]
                }
              ],
              "plaintext": "html-parse - HTML parser in Unison"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.horizontalRule"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 10,
                    "byteStart": 0
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#bold"
                    },
                    {
                      "uri": "https://share.unison-lang.org/@kaychaks/html-parse",
                      "$type": "pub.leaflet.richtext.facet#link"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 66,
                    "byteStart": 53
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 166,
                    "byteStart": 162
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 194,
                    "byteStart": 180
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#italic"
                    }
                  ]
                }
              ],
              "plaintext": "html-parse now has a second interpreter for the same parseHtmlText pipeline. Same parser, same malformed-HTML recovery, same entrypoint. But instead of producing Html, it produces typed markdown output, with an AST, rendered text, diagnostics, and typed fatal failures."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 37,
                    "byteStart": 24
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 65,
                    "byteStart": 56
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "The key insight is that parseHtmlText works through the HtmlBuild ability. Swapping the handler is all it takes to get a completely different output type:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "haskell",
              "plaintext": "\n-- existing\n\nbuildHtml do parseHtmlText \"<h1>Hello</h1>\"\n\n\n-- new\n\nbuildMarkdown do parseHtmlText \"<h1>Hello</h1>\"\n\n",
              "syntaxHighlightingTheme": "catppuccin-frappe"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 130,
                    "byteStart": 117
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "In place of a new parser or token walker, I decided to piggyback on the the malformed-HTML recovery stack machine in buildHtmlTree"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 2,
              "facets": [],
              "plaintext": "Output is a typed envelope, not just Text"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 230,
                    "byteStart": 226
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 255,
                    "byteStart": 238
                  },
                  "features": [
                    {
                      "uri": "https://share.unison-lang.org/@kaychaks/html-parse/code/releases/1.2.0/latest/types/internal/markdown/ConversionResult",
                      "$type": "pub.leaflet.richtext.facet#link"
                    }
                  ]
                }
              ],
              "plaintext": "I made a major mistake in the HTML parser to not have proper diagnostics and failure modelling. Hence, the first design decision I took for the markdown builder is that the result type is not just the final structured type or Text, it is ConverstionResult:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "url": "https://share.unison-lang.org/@kaychaks/html-parse/code/releases/1.2.0/latest/types/internal/markdown/ConversionResult",
              "$type": "pub.leaflet.blocks.iframe",
              "height": 404
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 63,
                    "byteStart": 55
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 113,
                    "byteStart": 102
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 167,
                    "byteStart": 164
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 219,
                    "byteStart": 212
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 227,
                    "byteStart": 223
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 251,
                    "byteStart": 247
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "This gives callers four independent things to inspect. rendered is empty if there is a fatal failure. diagnostics accumulates recoverable policy events regardless. ast is available even when rendering fails. And failure is None on the happy path, Some with a typed reason when something exceeded a bound."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "The AST itself has block and inline constructors covering the initial supported tags."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "url": "https://share.unison-lang.org/@kaychaks/html-parse/code/releases/1.2.0/latest/types/internal/markdown/Block",
              "$type": "pub.leaflet.blocks.iframe",
              "height": 516
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "url": "https://share.unison-lang.org/@kaychaks/html-parse/code/releases/1.2.0/latest/types/internal/markdown/Inline",
              "$type": "pub.leaflet.blocks.iframe",
              "height": 472
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 2,
              "facets": [],
              "plaintext": "Opinions encoded as types"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "Markdown, being simple on the surface, gets really weird when specifications like CommonMark or GFM are involved. I might enhance the library later to support all that weirdness, but for now, I am trying to remain dead simple."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "Hence, rather than leaving tag behaviour implicit, the library classifies every HTML tag into one of five classes:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "haskell",
              "plaintext": "\ntype TagClass\n\n  = SemanticTag    -- h1..h6, p, a, em, strong, ul, ol, li, code, pre, blockquote, hr, br\n\n  | UnsafeTag      -- script, style\n\n  | DeferredTag    -- img, table, video, and table structure tags\n\n  | TransparentTag -- known wrapper tags with no markdown equivalent\n\n  | UnknownTag     -- everything else\n\n",
              "syntaxHighlightingTheme": "catppuccin-frappe"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "Policy dispatch is then a pure function of config and class:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "haskell",
              "plaintext": "\ntype PolicyAction = \n  EmitDeferredRawHtml \n  | SuppressDeferredRawHtml\n\n\npolicy.apply : MarkdownPolicy -> TagClass -> PolicyAction\n\npolicy.apply policy = cases\n\n  DeferredTag -> \n    if allowDeferredRawHtml policy then \n      EmitDeferredRawHtml \n    else \n      SuppressDeferredRawHtml\n\n  _           -> EmitDeferredRawHtml\n\n",
              "syntaxHighlightingTheme": "catppuccin-frappe"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 9,
                    "byteStart": 0
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 36,
                    "byteStart": 24
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 99,
                    "byteStart": 85
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 114,
                    "byteStart": 104
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 153,
                    "byteStart": 142
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "UnsafeTag never reaches policy.apply because the subtree is dropped before dispatch. TransparentTag and UnknownTag are always unwrapped. Only DeferredTag is configurable."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "Failures are also an explicit closed enum:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "url": "https://share.unison-lang.org/@kaychaks/html-parse/code/releases/1.2.0/latest/types/internal/markdown/FailureReason",
              "$type": "pub.leaflet.blocks.iframe",
              "height": 446
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "There is no generic \"conversion failed\" variant. Each bound has its own constructor so callers can match on the exact reason without parsing strings."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 2,
              "facets": [],
              "plaintext": "The guard pipeline"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "Conversion runs in three guarded stages. Each stage either short-circuits with a typed failure or passes control forward:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": ""
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.image",
              "image": {
                "$type": "blob",
                "ref": {
                  "$link": "bafkreiekhuqrzmjdv4oxtlfx7oqj4czsuxg3cyfpvbp57n5xoo4cxmqctq"
                },
                "mimeType": "image/png",
                "size": 140204
              },
              "aspectRatio": {
                "width": 1262,
                "height": 1628
              }
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": ""
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 10,
                    "byteStart": 0
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 80,
                    "byteStart": 68
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 149,
                    "byteStart": 138
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 246,
                    "byteStart": 238
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 292,
                    "byteStart": 289
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 308,
                    "byteStart": 297
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "guardInput catches token-count violations before any tree is built. guardConvert catches structural violations after the AST is produced. guardRender catches output-size violations after rendering. A failure at any stage returns an empty rendered string, but earlier stages still populate ast and diagnostics so callers have something to inspect."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "The conversion stage also emits diagnostics for every recoverable policy event, regardless of whether the overall result is a failure. These are warnings, not errors, and they do not short-circuit the pipeline."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 2,
              "facets": [],
              "plaintext": "Wrapping up"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "So let's look at some examples of how all those design and implementation concepts work in practice."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 3,
              "facets": [],
              "plaintext": "Happy paths"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": ""
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "haskell",
              "plaintext": "\nbuildMarkdown do parseHtmlText \"<h1>Hello</h1><p>world</p>\"\n\n⧨\n\nConversionResult\n\n  (MarkdownAst\n\n    [ Heading 1 [PlainText \"Hello\"]\n\n    , Paragraph [PlainText \"world\"]\n\n    ])\n\n  \"# Hello\\n\\nworld\\n\\n\"\n\n  []\n\n  None\n\n",
              "syntaxHighlightingTheme": "catppuccin-frappe"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "Inline formatting with a link:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "haskell",
              "plaintext": "\nbuildMarkdown do\n\n  parseHtmlText \"<p>Some <strong>bold</strong> and <em>italic</em> with <a href=\\\"https://example.com\\\">docs</a>.</p>\"\n\n⧨\n\nConversionResult\n\n  (MarkdownAst\n\n    [ Paragraph\n\n        [ PlainText \"Some \"\n\n        , Strong [PlainText \"bold\"]\n\n        , PlainText \" and \"\n\n        , Emphasis [PlainText \"italic\"]\n\n        , PlainText \" with \"\n\n        , Link \"https://example.com\" \"\" [PlainText \"docs\"]\n\n        , PlainText \".\"\n\n        ]\n\n    ])\n\n  \"Some **bold** and *italic* with [docs](https://example.com).\\n\\n\"\n\n  []\n\n  None\n\n",
              "syntaxHighlightingTheme": "catppuccin-frappe"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 3,
              "facets": [],
              "plaintext": "Unhappy paths"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": ""
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "Unsafe content is dropped and does not appear in the rendered output:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "haskell",
              "plaintext": "\nresult = buildMarkdown do\n\n  parseHtmlText \"<p>ok</p><script>alert(1)</script><p>end</p>\"\n\nConversionResult.rendered result\n\n⧨\n\n\"ok\\n\\nend\\n\\n\"\n\n",
              "syntaxHighlightingTheme": "catppuccin-frappe"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 8,
                    "byteStart": 0
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "alert(1) never makes it into output. There is no diagnostic for this: unsafe subtrees are silently tombstoned by design because emitting a warning for script tags in real-world HTML would be overwhelming noise."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 64,
                    "byteStart": 0
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#bold"
                    }
                  ]
                }
              ],
              "plaintext": "Deferred tags emit raw HTML and a diagnostic with a source path:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "haskell",
              "plaintext": "\nresult = buildMarkdown do parseHtmlText \"<custom><img src=\\\"x\\\"/></custom>\"\n\ndiagnostics result\n\n⧨\n\n[ Diagnostic PolicyStage DeferredToRawHtml   \"img\"    (SourceRef \"img\"    [0, 0])\n\n, Diagnostic PolicyStage UnknownTagUnwrapped \"custom\" (SourceRef \"custom\" [0])\n\n]\n\n",
              "syntaxHighlightingTheme": "catppuccin-frappe"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 13,
                    "byteStart": 4
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 158,
                    "byteStart": 152
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "The SourceRef carries the tag name and the index path through the tree, so callers know exactly where in the input each policy event occurred. The path [0, 0] means the first child of the first root node."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 42,
                    "byteStart": 0
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#bold"
                    }
                  ]
                }
              ],
              "plaintext": "Hard limit produces a typed fatal failure:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "haskell",
              "plaintext": "\nlimits = maxInputTokens.set 1 MarkdownLimits.defaults\n\nconfig = MarkdownLimits.set limits MarkdownConfig.defaults\n\nresult = buildMarkdownWith config do parseHtmlText \"<p>a</p><p>b</p>\"\n\n(ConversionResult.rendered result, ConversionResult.failure result)\n\n⧨\n\n( \"\"\n\n, Some (Failure InputTokenLimitExceeded 1 6 \"input token count exceeded maxInputTokens\")\n\n)\n\n",
              "syntaxHighlightingTheme": "catppuccin-frappe"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 8,
                    "byteStart": 0
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 26,
                    "byteStart": 19
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "rendered is empty. failure tells you exactly which limit was breached, what the configured limit was, and what the actual count was."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 65,
                    "byteStart": 0
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#bold"
                    }
                  ]
                }
              ],
              "plaintext": "Policy toggle changes behavior without touching conversion logic:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "haskell",
              "plaintext": "\nstrict = MarkdownPolicy.allowDeferredRawHtml.set false MarkdownPolicy.defaults\n\nconfig = MarkdownPolicy.set strict MarkdownConfig.defaults\n\nresult = buildMarkdownWith config do parseHtmlText \"<p>text</p><img src=\\\"x\\\"/>\"\n\nConversionResult.rendered result\n\n⧨\n\n\"text\\n\\n\"\n\n",
              "syntaxHighlightingTheme": "catppuccin-frappe"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 33,
                    "byteStart": 5
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 44,
                    "byteStart": 39
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "With allowDeferredRawHtml = false, the <img> is suppressed entirely. The diagnostic still fires."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.horizontalRule"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.blockquote",
              "facets": [
                {
                  "index": {
                    "byteEnd": 57,
                    "byteStart": 55
                  },
                  "features": [
                    {
                      "uri": "https://share.unison-lang.org/@kaychaks/html-parse/contributions/2",
                      "$type": "pub.leaflet.richtext.facet#link"
                    }
                  ]
                }
              ],
              "plaintext": "All changes for this release are visible via this PR - #2"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.horizontalRule"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 101,
                    "byteStart": 89
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#italic"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 116,
                    "byteStart": 103
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#bold"
                    }
                  ]
                }
              ],
              "plaintext": "This is the next building block for my ultimate intent: an AT Protocol-based RSS reader. More to come, happy hacking 🤘"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.horizontalRule"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 34,
                    "byteStart": 0
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#highlight"
                    },
                    {
                      "$type": "pub.leaflet.richtext.facet#italic"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 502,
                    "byteStart": 34
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#italic"
                    }
                  ]
                }
              ],
              "textSize": "small",
              "plaintext": "If you have read this far, thanks! I am available for contract, temporary, freelance, and advisory roles where I can work closely with stakeholders or as an IC to solve hard product and engineering problems quickly. I am open to working in a variety of languages - Haskell, Rust, TypeScript/JavaScript, and, of course, Unison. I am fun to work with and have almost 20 years of experience, with expertise in functional programming, distributed systems, cloud-native architectures, and agentic workflows."
            }
          }
        ]
      }
    ]
  },
  "bskyPostRef": {
    "cid": "bafyreiec5k4i7atznghqko2fkm35dvee4bvzo2juresogzlcsudqsejnte",
    "uri": "at://did:plc:lvkhxfkdwqgwrpdek3h3q2gc/app.bsky.feed.post/3mf6xb7sa2k2u",
    "commit": {
      "cid": "bafyreiezykrwmj2y4agaaflfjcm72jd5pch5yv6gvny264qkbdf2qml4gm",
      "rev": "3mf6xb7uvsx2c"
    },
    "validationStatus": "valid"
  },
  "description": "New version of `html-parse` library",
  "publishedAt": "2026-02-19T06:20:41.935Z"
}