packages feed

mcp-hoogle-0.1.0: src/McpHoogle/Format.hs

-- | Rendering of Hoogle search results into human-readable text.
--
-- Hoogle's 'Target' type contains HTML markup (e.g. @\<s0\>map\<\/s0\>@)
-- which is meaningless in a plain-text MCP response. This module strips
-- the HTML and formats each result with its package, module, docs, and URL
-- so an AI assistant can read them directly.
module McpHoogle.Format
  ( formatTarget
  , formatTargets
  , stripHtmlTags
  )
where

import Data.Text (Text)
import Data.Text qualified as Text
import Hoogle (Target(..))

-- | Format a single Hoogle 'Target' into a readable markdown-ish block.
--
-- Includes the item signature (HTML-stripped), package name, module name,
-- documentation excerpt, and a link to the Hackage page.
formatTarget :: Target -> Text
formatTarget target = Text.unlines
  [ "## " <> stripHtmlTags (Text.pack (targetItem target))
  , case targetPackage target of
      Just (package, _url) -> "Package: " <> Text.pack package
      Nothing -> ""
  , case targetModule target of
      Just (moduleName, _url) -> "Module: " <> Text.pack moduleName
      Nothing -> ""
  , if null (targetDocs target)
      then ""
      else "\n" <> stripHtmlTags (Text.pack (targetDocs target))
  , "URL: " <> Text.pack (targetURL target)
  ]

-- | Format a list of targets separated by horizontal rules.
-- Returns a "No results found." message for an empty list.
formatTargets :: [Target] -> Text
formatTargets [] = "No results found."
formatTargets targets =
  Text.intercalate "\n---\n\n" (map formatTarget targets)

-- | Strip HTML tags from text, keeping only the text content.
--
-- Hoogle wraps function names in @\<s0\>...\<\/s0\>@ spans and uses
-- other HTML for formatting. This does a single-pass removal of all
-- angle-bracketed sequences.
stripHtmlTags :: Text -> Text
stripHtmlTags = go False
  where
    go :: Bool -> Text -> Text
    go _inTag input = case Text.uncons input of
      Nothing -> Text.empty
      Just ('<', rest) -> go True rest
      Just (char, rest)
        | _inTag -> case char of
            '>' -> go False rest
            _   -> go True rest
        | otherwise -> Text.cons char (go False rest)