packages feed

gachi-scaffold-0.1.0.0: src/Gachi/Scaffold/Markdown.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Module: Gachi.Scaffold.Markdown
-- Description: Lean GitHub-Flavored Markdown (GFM) AST and deterministic pretty-printer
--
-- This module implements a lightweight, pure-Haskell abstract syntax tree for
-- GitHub-Flavored Markdown (GFM) and CommonMark, paired with a deterministic,
-- zero-allocation pretty-printer.
--
-- = Architectural Context & Invariants
--
-- In Gachi's infrastructure model (per Note [Ecosystem Standardization Invariants]
-- [ref:note-ecosystem-standardization]), repository assets and operational documents
-- must be emitted with structural precision.
--
-- Rather than relying on fragile, ad-hoc string concatenation (@T.unlines@) or
-- dragging in heavyweight external engines with hundreds of transitive dependencies
-- (such as full Pandoc), this module provides a lean, strongly-typed document
-- generation layer.
--
-- == Key Guarantees
--
-- 1. __Pure & Total:__ The pretty-printer 'renderMarkdown' is a pure, total
--    function with zero runtime I/O, zero network calls, and zero external C FFI.
-- 2. __Structural Invariance:__
--    * Table columns are dynamically measured across all rows and padded to
--      uniform widths with aligned separator lines (@| :--- | :--- |@).
--    * Block spacing follows standard CommonMark double-newline conventions.
--    * Links, anchors, code fencing, and Tagref pointers are structurally valid
--      by construction.
-- 3. __Lightweight Footprint:__ Zero dependencies beyond @base@ and @text@.
--
-- = Worked Example
--
-- >>> import Gachi.Scaffold.Markdown
-- >>> let doc = [ h1 "Project Title", para "A brief overview.", table ["Col 1", "Col 2"] [[plain "Val A", plain "Val B"]] ]
-- >>> renderMarkdown doc
-- "# Project Title\n\nA brief overview.\n\n| Col 1 | Col 2 |\n| :--- | :--- |\n| Val A | Val B |\n"
module Gachi.Scaffold.Markdown
  ( -- * Document Abstract Syntax Tree
    Block(..)
  , Inline(..)

    -- * Pretty-Printing Operations
  , renderMarkdown
  , renderBlock
  , renderInline

    -- * Block Constructors
  , h1
  , h2
  , h3
  , h4
  , para
  , paraInlines
  , codeBlock
  , plainCodeBlock
  , bulletList
  , orderedList
  , table
  , hr
  , rawBlock

    -- * Inline Constructors
  , plain
  , bold
  , italic
  , code
  , link
  , linkInline
  , tagRef
  , tagDef
  , inlines
  ) where

import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)

--------------------------------------------------------------------------------
-- Abstract Syntax Tree: Inlines
--------------------------------------------------------------------------------

-- | Inline typographical and structural elements within a Markdown block.
data Inline
  = Plain !Text
    -- ^ Unformatted plain text.
  | Bold !Text
    -- ^ Strongly emphasized text: @**text**@.
  | Italic !Text
    -- ^ Emphasized text: @*text*@.
  | Code !Text
    -- ^ Inline monospace code span: @`code`@.
  | Link !Text !Text
    -- ^ Hyperlink with display label and destination URL/anchor: @[label](url)@.
  | TagRef !Text
    -- ^ Mechanical Tagref reference pointer (formatted as @ref:slug@ in brackets).
  | TagDef !Text
    -- ^ Mechanical Tagref definition anchor (formatted as @tag:slug@ in brackets).
  | Inlines ![Inline]
    -- ^ Compound sequence of inline elements concatenated together.
  deriving stock (Eq, Show, Generic)

instance Semigroup Inline where
  Inlines xs <> Inlines ys = Inlines (xs ++ ys)
  Inlines xs <> y          = Inlines (xs ++ [y])
  x          <> Inlines ys = Inlines (x : ys)
  Plain ""   <> y          = y
  x          <> Plain ""   = x
  x          <> y          = Inlines [x, y]

instance Monoid Inline where
  mempty = Plain ""

--------------------------------------------------------------------------------
-- Abstract Syntax Tree: Blocks
--------------------------------------------------------------------------------

-- | Block-level document elements in GitHub-Flavored Markdown.
data Block
  = Heading !Int !Text
    -- ^ Section heading with depth level (1–6) and title text.
  | Paragraph ![Inline]
    -- ^ Standard paragraph comprising a sequence of inline elements.
  | FencedCode !(Maybe Text) !Text
    -- ^ Code block enclosed in triple backticks with optional language tag.
  | BulletList ![Inline]
    -- ^ Unordered list of bulleted items.
  | OrderedList ![Inline]
    -- ^ Ordered numbered list of items.
  | Table ![Text] ![[Inline]]
    -- ^ GitHub-Flavored Markdown table with column headers and cell rows.
  | ThematicBreak
    -- ^ Horizontal thematic divider: @---@.
  | RawBlock !Text
    -- ^ Pre-rendered raw text block preserved without transformation.
  deriving stock (Eq, Show, Generic)

--------------------------------------------------------------------------------
-- Ergonomic Block Smart Constructors
--------------------------------------------------------------------------------

-- | Creates a Level 1 Heading (@# Title@).
h1 :: Text -> Block
h1 = Heading 1

-- | Creates a Level 2 Heading (@## Title@).
h2 :: Text -> Block
h2 = Heading 2

-- | Creates a Level 3 Heading (@### Title@).
h3 :: Text -> Block
h3 = Heading 3

-- | Creates a Level 4 Heading (@#### Title@).
h4 :: Text -> Block
h4 = Heading 4

-- | Creates a paragraph from plain text.
para :: Text -> Block
para t = Paragraph [Plain t]

-- | Creates a paragraph from a sequence of structured inline elements.
paraInlines :: [Inline] -> Block
paraInlines = Paragraph

-- | Creates a syntax-highlighted fenced code block.
codeBlock :: Text -> Text -> Block
codeBlock lang = FencedCode (Just lang)

-- | Creates an untyped fenced code block without language annotation.
plainCodeBlock :: Text -> Block
plainCodeBlock = FencedCode Nothing

-- | Creates an unordered bulleted list from inline items.
bulletList :: [Inline] -> Block
bulletList = BulletList

-- | Creates an ordered numbered list from inline items.
orderedList :: [Inline] -> Block
orderedList = OrderedList

-- | Creates a formatted GFM table from headers and row cells.
table :: [Text] -> [[Inline]] -> Block
table = Table

-- | Creates a horizontal thematic break divider (@---@).
hr :: Block
hr = ThematicBreak

-- | Creates a raw unescaped block preserved without modification.
rawBlock :: Text -> Block
rawBlock = RawBlock

--------------------------------------------------------------------------------
-- Ergonomic Inline Smart Constructors
--------------------------------------------------------------------------------

-- | Creates a plain text inline element.
plain :: Text -> Inline
plain = Plain

-- | Creates a bold inline element (@**text**@).
bold :: Text -> Inline
bold = Bold

-- | Creates an italic inline element (@*text*@).
italic :: Text -> Inline
italic = Italic

-- | Creates an inline monospace code span (@`code`@).
code :: Text -> Inline
code = Code

-- | Creates an inline hyperlink (@[text](url)@).
link :: Text -> Text -> Inline
link = Link

-- | Creates an inline hyperlink from a rendered 'Inline' label (@[inline](url)@).
linkInline :: Inline -> Text -> Inline
linkInline inl = Link (renderInline inl)

-- | Constructs a dynamic Tagref reference pointer (formatted as @ref:slug@ in brackets).
tagRef :: Text -> Inline
tagRef = TagRef

-- | Constructs a dynamic Tagref definition anchor (formatted as @tag:slug@ in brackets).
tagDef :: Text -> Inline
tagDef = TagDef

--------------------------------------------------------------------------------
-- Pretty-Printing: Inlines
--------------------------------------------------------------------------------

-- | Renders an 'Inline' element to its canonical Markdown representation.
--
-- Time complexity: @O(L)@ proportional to inline text length.
renderInline :: Inline -> Text
renderInline (Plain t)    = t
renderInline (Bold t)     = "**" <> t <> "**"
renderInline (Italic t)   = "*" <> t <> "*"
renderInline (Code t)     = "`" <> t <> "`"
renderInline (Link t u)   = "[" <> t <> "](" <> u <> ")"
renderInline (TagRef slug) = "[" <> "ref:" <> slug <> "]"
renderInline (TagDef slug) = "[" <> "tag:" <> slug <> "]"
renderInline (Inlines is)  = T.concat (map renderInline is)

-- | Constructs a compound sequence of inlines.
inlines :: [Inline] -> Inline
inlines = Inlines

--------------------------------------------------------------------------------
-- Pretty-Printing: Blocks
--------------------------------------------------------------------------------

-- | Renders a sequence of 'Block' elements into a complete Markdown document.
--
-- Blocks are separated by standard blank lines adhering to CommonMark rules.
--
-- Time complexity: @O(N)@ proportional to total document nodes and cell counts.
renderMarkdown :: [Block] -> Text
renderMarkdown blocks =
  T.intercalate "\n" (map renderBlock blocks)

-- | Renders a single 'Block' element to formatted Markdown text.
renderBlock :: Block -> Text
renderBlock (Heading level title) =
  let hashes = T.replicate (max 1 (min 6 level)) "#"
  in hashes <> " " <> title <> "\n"

renderBlock (Paragraph inls) =
  T.concat (map renderInline inls) <> "\n"

renderBlock (FencedCode maybeLang codeContent) =
  let lang = fromMaybe "" maybeLang
      cleanContent = T.dropWhileEnd (== '\n') codeContent
  in "```" <> lang <> "\n" <> cleanContent <> "\n```\n"

renderBlock (BulletList items) =
  let renderItem item = "- " <> renderInline item
  in T.unlines (map renderItem items)

renderBlock (OrderedList items) =
  let renderItem idx item = T.show (idx :: Int) <> ". " <> renderInline item
  in T.unlines (zipWith renderItem [1 ..] items)

renderBlock (Table headers rows) =
  renderTableLayout headers rows

renderBlock ThematicBreak =
  "---\n"

renderBlock (RawBlock raw) =
  T.dropWhileEnd (== '\n') raw <> "\n"

--------------------------------------------------------------------------------
-- Table Formatting Mechanics
--------------------------------------------------------------------------------

-- | Formats a GitHub-Flavored Markdown table with auto-aligned column boundaries.
--
-- Dynamically evaluates cell text lengths across all headers and rows, computing
-- the minimum required padding per column and emitting clean separator lines.
renderTableLayout :: [Text] -> [[Inline]] -> Text
renderTableLayout headers rows =
  let renderedRows = map (map renderInline) rows
      colCount = length headers
      -- Pad or truncate each row to match header column count
      normalizedRows = map (normalizeRow colCount) renderedRows
      -- Compute maximum width per column (at least 3 characters for :---)
      allRowsWithHeader = headers : normalizedRows
      colWidths = [ maximum (3 : map (T.length . safeIndex c) allRowsWithHeader)
                  | c <- [0 .. colCount - 1]
                  ]
      -- Format header line
      headerLine = formatRow colWidths headers
      -- Format separator line (| :--- | :--- |)
      sepLine = formatSepRow colWidths
      -- Format data rows
      dataLines = map (formatRow colWidths) normalizedRows
  in T.unlines (headerLine : sepLine : dataLines)
  where
    normalizeRow targetLen r
      | length r >= targetLen = take targetLen r
      | otherwise             = r ++ replicate (targetLen - length r) ""

    safeIndex idx xs
      | idx < length xs = xs !! idx
      | otherwise       = ""

    formatRow widths cells =
      let padded = zipWith padCell widths cells
      in "| " <> T.intercalate " | " padded <> " |"

    formatSepRow widths =
      let seps = map (\w -> ":" <> T.replicate (max 2 (w - 1)) "-") widths
      in "| " <> T.intercalate " | " seps <> " |"

    padCell width cell =
      let padding = max 0 (width - T.length cell)
      in cell <> T.replicate padding " "