From 64344583f33e5388ebe4e0f7f064ffae284f55cd Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:09:59 -0400 Subject: [PATCH 1/8] fix(rawhtml): group orphan opener/closer raw-HTML blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CommonMark "type 6" HTML blocks end at the next blank line, so markdown like
**bold** content
reaches the renderer as three blocks: an opener `RawBlock` carrying `"
\n"`, a `Para`, and a closer `RawBlock` with `"
\n"`. The current rawNode wraps each raw blob in its own `` element to keep xmlhtml from mangling the bytes; the side effect is that the `
` open and close tags get trapped inside their wrappers and the markdown paragraph ends up a sibling of the (empty) details element rather than its child. New `Heist.Extra.Splices.Pandoc.RawHtmlGroup.groupRawHtmlBlocks` pass walks the AST (via `Text.Pandoc.Walk`, so nested block lists are covered too) and rewrites those orphan triplets into a `B.Div` carrying the tag in its `"tag"` attribute. The `Div` arm of `rpBlock'` already turns that into the named element. It now also strips the `"tag"` directive before serialising attributes so the override doesn't leak as a literal `tag="…"` attribute on the rendered element. Tests: 12 unit tests pin the parser and grouping behaviour (issue example, empty group, orphan open/close, consecutive pairs, same-tag nesting via depth counting, self-closing rejection, balanced-in-one-block rejection, case-insensitive matching, attribute tolerance, hyphenated custom-element names, mismatched-tag orphan), plus an end-to-end integration test through `renderPandocWith` that asserts the markdown paragraph lands inside `
` with no `` wrapper. Closes srid/emanote#433. --- CHANGELOG.md | 1 + heist-extra.cabal | 5 +- .../Extra/Splices/Pandoc/RawHtmlGroup.hs | 114 +++++++++++++++ src/Heist/Extra/Splices/Pandoc/Render.hs | 17 ++- .../Extra/Splices/Pandoc/RawHtmlGroupSpec.hs | 133 ++++++++++++++++++ test/Heist/Extra/Splices/Pandoc/RenderSpec.hs | 44 ++++++ test/Spec.hs | 2 + 7 files changed, 312 insertions(+), 4 deletions(-) create mode 100644 src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs create mode 100644 test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dafa58..1d5634e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add static math rendering for `$...$` and `$$...$$` via `texmath` (LaTeX → MathML at build time) - Wrap raw HTML/inline blobs in a unique `` element (with `display: contents`) instead of `
`/`` ([#13](https://github.com/srid/heist-extra/pull/13)). Avoids xmlhtml's "div cannot contain text looking like its end tag" crash when raw HTML contains a literal `
` — most painfully, mermaid SVG with `
` HTML labels. Fixes [srid/emanote#119](https://github.com/srid/emanote/issues/119). - Pandoc `Table` rendering now applies the AST fields it had been silently dropping ([#15](https://github.com/srid/heist-extra/pull/15)): per-column alignment from `ColSpec` (with cell-level `Alignment` overriding the column default), column widths via a generated ``, cell `RowSpan`/`ColSpan` as `rowspan`/`colspan` attributes, row & cell `Attr` merged into the rendered ``/``/``, and `TableFoot` rows rendered into ``. Captions are still skipped — commonmark-hs doesn't emit them. Pure helpers (`alignmentStyle`, `colSpecsToColgroup`, `cellSpanAttrs`, `cellColumnIndices`, `mergeStyleKVs`) live in the new `Heist.Extra.Splices.Pandoc.Render.Internal` module so consumers get a clean public API. Fixes [srid/emanote#27](https://github.com/srid/emanote/issues/27). +- Group orphan opener/closer raw-HTML blocks around the markdown content between them, so the renderer emits a real DOM element instead of two stranded `` wrappers. CommonMark "type 6" HTML blocks end at the next blank line, which makes Pandoc split `
\n\nbody\n\n
` into three blocks: an opener `RawBlock`, a `Para`, and a closer `RawBlock`. Without grouping, each raw blob lives in its own wrapper and the markdown paragraph ends up a sibling of the (empty) details element rather than its child. The new `Heist.Extra.Splices.Pandoc.RawHtmlGroup.groupRawHtmlBlocks` pass walks the AST and rewrites those triplets into a `B.Div` carrying the tag in its `"tag"` attribute, which the renderer already turns into the named element. The `Div` arm of the renderer now also strips that `"tag"` directive from the serialized attributes so it doesn't leak as a literal `tag="…"` on the output element. Fixes [srid/emanote#433](https://github.com/srid/emanote/issues/433). ## 0.4.0.0 (2025-08-19) diff --git a/heist-extra.cabal b/heist-extra.cabal index d376f25..9aaa9bf 100644 --- a/heist-extra.cabal +++ b/heist-extra.cabal @@ -99,6 +99,7 @@ library Heist.Extra.Splices.Pandoc.Attr Heist.Extra.Splices.Pandoc.Ctx Heist.Extra.Splices.Pandoc.Footnotes + Heist.Extra.Splices.Pandoc.RawHtmlGroup Heist.Extra.Splices.Pandoc.Render Heist.Extra.Splices.Pandoc.Render.Internal Heist.Extra.Splices.Pandoc.Skylighting @@ -111,7 +112,9 @@ test-suite test type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Spec.hs - other-modules: Heist.Extra.Splices.Pandoc.RenderSpec + other-modules: + Heist.Extra.Splices.Pandoc.RawHtmlGroupSpec + Heist.Extra.Splices.Pandoc.RenderSpec default-language: Haskell2010 default-extensions: ImportQualifiedPost OverloadedStrings ghc-options: -Wall diff --git a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs new file mode 100644 index 0000000..fd309b5 --- /dev/null +++ b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs @@ -0,0 +1,114 @@ +{- | Group orphan raw-HTML opener / closer block pairs around the markdown +content between them, so the renderer can emit a real DOM element that +nests the content as children. + +Background. CommonMark "type 6" HTML blocks (any block-level start tag) end +at the next blank line. Markdown like + +@ +\ + +**bold** content + +\ +@ + +reaches a Pandoc renderer as three blocks: a 'B.RawBlock' with +@"\\\n"@, a 'B.Para' for the paragraph, and another 'B.RawBlock' +with @"\\\n"@. Heist-extra's renderer wraps each raw blob in +its own @\@ element to keep xmlhtml from mangling the bytes; the +side effect is that the @\@ open and close get trapped inside +those wrappers and the markdown paragraph ends up a sibling of the (now +empty) details element rather than its child. See @srid/emanote#433@. + +This pass walks a block list and, when it sees an unbalanced opening tag +followed downstream by a matching closing tag (depth counted only against +opens of the same tag name), replaces that span with a 'B.Div' carrying +the tag name in the @"tag"@ attribute. The render path already turns @Div@ +with a @"tag"@ attr into the named element, so the markdown content lands +as a real DOM child. + +Tags without a matching closer are left as raw blocks — emitting a +synthetic close would change the input's semantics. Tag names are +compared case-insensitively to follow HTML's own rules. Attributes on +the opener (e.g. @\
@) are dropped when we group; that is +deliberate scope until a real case demands otherwise. A tag like +@\foo\@ that already balances inside one +'B.RawBlock' is left alone — the renderer handles balanced raw-HTML +fragments correctly today. +-} +module Heist.Extra.Splices.Pandoc.RawHtmlGroup ( + groupRawHtmlBlocks, +) where + +import Data.Char (isSpace) +import Data.Text qualified as T +import Text.Pandoc.Definition qualified as B + +groupRawHtmlBlocks :: [B.Block] -> [B.Block] +groupRawHtmlBlocks = \case + [] -> [] + b : rest + | Just tag <- openerTag b + , Just (inner, after) <- splitAtMatchingCloser tag rest -> + B.Div ("", [], [("tag", tag)]) (groupRawHtmlBlocks inner) + : groupRawHtmlBlocks after + | otherwise -> b : groupRawHtmlBlocks rest + +-- | Read @b@ as an opening raw-HTML tag, returning the (lowercased) tag name. +openerTag :: B.Block -> Maybe Text +openerTag = \case + B.RawBlock (B.Format "html") s -> parseOpener (T.strip s) + _ -> Nothing + where + parseOpener s = do + afterLT <- T.stripPrefix "<" s + guard $ not ("/" `T.isPrefixOf` afterLT) + let (name, rest) = T.span isTagNameChar afterLT + guard $ not (T.null name) + -- Reject self-closing forms (@\
@): the last non-space char + -- before '>' is '/'. + let inside = T.takeWhile (/= '>') rest + guard $ not ("/" `T.isSuffixOf` T.stripEnd inside) + -- The opener must be the only thing on this raw block; if anything + -- non-whitespace follows the '>', we're looking at a single-block + -- balanced fragment and must not group it. + afterGT <- T.stripPrefix ">" (T.dropWhile (/= '>') rest) + guard $ T.all isSpace afterGT + pure $ T.toLower name + +-- | Read @b@ as a closing tag for @tag@. +closerTag :: Text -> B.Block -> Bool +closerTag tag = \case + B.RawBlock (B.Format "html") s -> case T.stripPrefix " + let (name, after) = T.span isTagNameChar rest + afterClose = T.drop 1 (T.dropWhile (/= '>') after) + in T.toLower name == tag && T.all isSpace afterClose + Nothing -> False + _ -> False + +isTagNameChar :: Char -> Bool +isTagNameChar c = + (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c == '-' + +{- | Walk forward, tracking nesting depth of @tag@. On the matching closer +(depth back to zero) split into the inner span and the remainder. 'Nothing' +means the opener is orphan at this level. +-} +splitAtMatchingCloser :: Text -> [B.Block] -> Maybe ([B.Block], [B.Block]) +splitAtMatchingCloser tag = go (1 :: Int) [] + where + go _ _ [] = Nothing + go depth acc (b : bs) + | closerTag tag b = + if depth == 1 + then Just (reverse acc, bs) + else go (depth - 1) (b : acc) bs + | Just t <- openerTag b + , t == tag = + go (depth + 1) (b : acc) bs + | otherwise = go depth (b : acc) bs diff --git a/src/Heist/Extra/Splices/Pandoc/Render.hs b/src/Heist/Extra/Splices/Pandoc/Render.hs index 7910897..5ba9a9b 100644 --- a/src/Heist/Extra/Splices/Pandoc/Render.hs +++ b/src/Heist/Extra/Splices/Pandoc/Render.hs @@ -24,6 +24,7 @@ import Heist.Extra.Splices.Pandoc.Ctx ( RenderFeatures (..), rewriteClass, ) +import Heist.Extra.Splices.Pandoc.RawHtmlGroup (groupRawHtmlBlocks) import Heist.Extra.Splices.Pandoc.Render.Internal ( alignmentStyle, cellColumnIndices, @@ -40,9 +41,14 @@ import Text.Pandoc.Definition (Pandoc (..)) import Text.Pandoc.Walk as W import Text.XmlHtml qualified as X +-- | Pre-group orphan opener/closer raw-HTML pairs at every block-list level. +preprocess :: Pandoc -> Pandoc +preprocess = W.walk groupRawHtmlBlocks + renderPandocWith :: RenderCtx -> Pandoc -> HI.Splice Identity -renderPandocWith ctx (Pandoc _meta blocks) = - foldMapM (rpBlock ctx) blocks +renderPandocWith ctx doc = + let Pandoc _meta blocks = preprocess doc + in foldMapM (rpBlock ctx) blocks rpBlock :: RenderCtx -> B.Block -> HI.Splice Identity rpBlock ctx@RenderCtx {..} b = do @@ -145,7 +151,7 @@ rpBlock' ctx@RenderCtx {..} b = case b of tfoot <- wrapSection "tfoot" "td" frows pure $ cg <> thead <> tbody <> tfoot B.Div attr bs -> - one . X.Element (getTag "div" attr) (rpAttr $ rewriteClass ctx attr) + one . X.Element (getTag "div" attr) (rpAttr $ rewriteClass ctx (dropTagAttr attr)) <$> foldMapM (rpBlock ctx) bs B.Figure attr _caption bs -> -- TODO: support caption @@ -154,6 +160,11 @@ rpBlock' ctx@RenderCtx {..} b = case b of getTag defaultTag (_, _, Map.fromList -> attrs) = Map.lookup "tag" attrs & fromMaybe defaultTag + -- The "tag" entry is a directive picked up by 'getTag' to override the + -- element name; it must not survive into the rendered HTML as a literal + -- @tag="…"@ attribute. + dropTagAttr (i, cs, kvs) = (i, cs, filter ((/= "tag") . fst) kvs) + headerSplices headerId innerSplice = do "header:id" ## HI.textSplice headerId "inlines" ## innerSplice diff --git a/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs b/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs new file mode 100644 index 0000000..7dac0f5 --- /dev/null +++ b/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs @@ -0,0 +1,133 @@ +{- | Tests for "Heist.Extra.Splices.Pandoc.RawHtmlGroup". + +The pass solves @srid/emanote#433@: orphan opener/closer raw-HTML blocks +end up wrapping a markdown paragraph as siblings rather than parents +unless the renderer can see a real DOM 'B.Div' instead of two stranded +'B.RawBlock's. These tests pin the AST shape produced for every variant +that mattered enough to write code for. +-} +module Heist.Extra.Splices.Pandoc.RawHtmlGroupSpec (spec) where + +import Data.Text (Text) +import Heist.Extra.Splices.Pandoc.RawHtmlGroup (groupRawHtmlBlocks) +import Test.Hspec +import Text.Pandoc.Definition qualified as B +import Prelude + +-- | Block helpers that keep the test cases readable. +raw :: Text -> B.Block +raw s = B.RawBlock (B.Format "html") s + +para :: Text -> B.Block +para s = B.Para [B.Str s] + +-- | A 'B.Div' with the named tag and no other attrs — what the pass produces. +taggedDiv :: Text -> [B.Block] -> B.Block +taggedDiv tag = B.Div ("", [], [("tag", tag)]) + +spec :: Spec +spec = describe "groupRawHtmlBlocks (srid/emanote#433)" $ do + it "groups the issue's example: opener, paragraph, closer" $ do + -- The exact Pandoc AST emitted by parseNoteMarkdown on the issue's MD. + let input = + [ para "aaaa" + , raw "
\n" + , para "bbb" + , raw "
\n" + , para "eee" + ] + expected = + [ para "aaaa" + , taggedDiv "details" [para "bbb"] + , para "eee" + ] + groupRawHtmlBlocks input `shouldBe` expected + + it "produces an empty Div when opener and closer are adjacent" $ do + -- The "empty group" case the branch is named after: nothing between + -- the open and close raw blocks. + let input = [raw "
\n", raw "
\n"] + expected = [taggedDiv "details" []] + groupRawHtmlBlocks input `shouldBe` expected + + it "leaves an opener with no matching closer untouched" $ do + let input = [raw "
\n", para "stuck open"] + groupRawHtmlBlocks input `shouldBe` input + + it "leaves a closer with no opener untouched" $ do + let input = [para "lone", raw "
\n"] + groupRawHtmlBlocks input `shouldBe` input + + it "groups consecutive opener/closer pairs independently" $ do + let input = + [ raw "
\n" + , para "x" + , raw "
\n" + , raw "\n" + ] + expected = + [ taggedDiv "details" [para "x"] + , taggedDiv "aside" [para "y"] + ] + groupRawHtmlBlocks input `shouldBe` expected + + it "handles same-tag nesting via depth counting" $ do + -- Two opens before a close: outer must wrap the inner pair. + let input = + [ raw "
\n" + , raw "
\n" + , para "inner" + , raw "
\n" + , para "between" + , raw "
\n" + ] + expected = + [ taggedDiv + "details" + [ taggedDiv "details" [para "inner"] + , para "between" + ] + ] + groupRawHtmlBlocks input `shouldBe` expected + + it "ignores self-closing forms like
" $ do + let input = [raw "
\n", para "after"] + groupRawHtmlBlocks input `shouldBe` input + + it "ignores raw blocks that already balance internally" $ do + -- Single-line `x` doesn't get split into open/close + -- by Pandoc, so it doesn't match the opener pattern. + let input = [raw "
foo
\n", para "after"] + groupRawHtmlBlocks input `shouldBe` input + + it "matches tag names case-insensitively" $ do + let input = [raw "
\n", para "x", raw "
\n"] + expected = [taggedDiv "details" [para "x"]] + groupRawHtmlBlocks input `shouldBe` expected + + it "accepts opener attributes (and drops them on the produced Div)" $ do + -- Attribute support on the output is out of scope for this pass; + -- what matters is that the attributes don't trip the parser. + let input = + [ raw "
\n" + , para "x" + , raw "
\n" + ] + expected = [taggedDiv "details" [para "x"]] + groupRawHtmlBlocks input `shouldBe` expected + + it "groups custom-element tag names with hyphens" $ do + let input = + [ raw "\n" + , para "x" + , raw "\n" + ] + expected = [taggedDiv "my-card" [para "x"]] + groupRawHtmlBlocks input `shouldBe` expected + + it "leaves orphan tags whose closers belong to a different element" $ do + -- An
downstream stays open. + let input = [raw "
\n"] + groupRawHtmlBlocks input `shouldBe` input diff --git a/test/Heist/Extra/Splices/Pandoc/RenderSpec.hs b/test/Heist/Extra/Splices/Pandoc/RenderSpec.hs index 72fc46d..2df312f 100644 --- a/test/Heist/Extra/Splices/Pandoc/RenderSpec.hs +++ b/test/Heist/Extra/Splices/Pandoc/RenderSpec.hs @@ -13,6 +13,7 @@ import Heist qualified as H import Heist.Extra.Splices.Pandoc.Ctx (emptyRenderCtx) import Heist.Extra.Splices.Pandoc.Render ( rawNode, + renderPandocWith, rpBlock, ) import Heist.Extra.Splices.Pandoc.Render.Internal ( @@ -342,6 +343,17 @@ spec = do out `shouldSatisfy` ("id='cell-id'" `BS.isInfixOf`) out `shouldSatisfy` ("cell-class" `BS.isInfixOf`) + it "drops the `tag` directive from the rendered Div's attributes" $ \hs -> do + -- `tag` is a directive picked up by getTag to override the element + -- name; it must not survive into the rendered HTML as a literal + -- @tag="…"@ attribute. Regression for the fix that landed alongside + -- the RawHtmlGroup pass (srid/emanote#433). + let div_ = B.Div ("", [], [("tag", "details"), ("data-keep", "yes")]) [B.Para [B.Str "x"]] + out <- renderBlockHtml hs div_ + out `shouldSatisfy` (" do -- Cell already carries a `style` (e.g. from raw HTML in the source); -- column 0 is right-aligned, so the renderer adds another `style`. @@ -358,6 +370,38 @@ spec = do out `shouldSatisfy` ("color: red" `BS.isInfixOf`) out `shouldSatisfy` ("text-align: right" `BS.isInfixOf`) + -- End-to-end: drive renderPandocWith with the AST `Emanote.Model.Note.parseNoteMarkdown` + -- prints from the issue's reproducer, and assert the bytes that come out + -- of xmlhtml's fragment renderer. Without the RawHtmlGroup pass the + -- markdown paragraph would be a sibling of an empty `
`. + beforeAll initEmptyHeistState $ + describe "renderPandocWith (srid/emanote#433, integration)" $ do + it "nests the markdown paragraph inside the
element" $ \hs -> do + let doc = + B.Pandoc + mempty + [ B.Para [B.Str "aaaa"] + , B.RawBlock (B.Format "html") "
\n" + , B.Para + [ B.Strong [B.Str "bbb"] + , B.Space + , B.Str "ccc" + , B.Space + , B.Emph [B.Str "ddd"] + ] + , B.RawBlock (B.Format "html") "
\n" + , B.Para [B.Str "eee"] + ] + nodes = runIdentity $ H.evalHeistT (renderPandocWith emptyRenderCtx doc) (X.TextNode "") hs + out <- renderHtml nodes + -- The whole point: the inner paragraph sits between
and + --
, with no wrapper around either tag. + out `shouldSatisfy` ("

" `BS.isInfixOf`) + out `shouldSatisfy` ("

" `BS.isInfixOf`) + out `shouldSatisfy` (not . ("rawhtml" `BS.isInfixOf`)) + out `shouldSatisfy` ("bbb" `BS.isInfixOf`) + out `shouldSatisfy` ("ddd" `BS.isInfixOf`) + -- * Test helpers for the integration suite. {- | Build a fresh 'H.HeistState Identity' with no templates loaded. diff --git a/test/Spec.hs b/test/Spec.hs index 539fb90..7bd0f89 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -1,8 +1,10 @@ module Main (main) where +import Heist.Extra.Splices.Pandoc.RawHtmlGroupSpec qualified as RawHtmlGroupSpec import Heist.Extra.Splices.Pandoc.RenderSpec qualified as RenderSpec import Test.Hspec (hspec) main :: IO () main = hspec $ do RenderSpec.spec + RawHtmlGroupSpec.spec From 970c9a373b0c7eb10c76a20f44a44fb8547d7066 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:13:49 -0400 Subject: [PATCH 2/8] =?UTF-8?q?fix(police):=20hlint=20=E2=80=94=20use=20is?= =?UTF-8?q?AsciiLower/isAsciiUpper/isDigit,=20eta-reduce=20raw=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs | 8 ++------ test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs index fd309b5..39a0d48 100644 --- a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs +++ b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs @@ -41,7 +41,7 @@ module Heist.Extra.Splices.Pandoc.RawHtmlGroup ( groupRawHtmlBlocks, ) where -import Data.Char (isSpace) +import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace) import Data.Text qualified as T import Text.Pandoc.Definition qualified as B @@ -89,11 +89,7 @@ closerTag tag = \case _ -> False isTagNameChar :: Char -> Bool -isTagNameChar c = - (c >= 'a' && c <= 'z') - || (c >= 'A' && c <= 'Z') - || (c >= '0' && c <= '9') - || c == '-' +isTagNameChar c = isAsciiLower c || isAsciiUpper c || isDigit c || c == '-' {- | Walk forward, tracking nesting depth of @tag@. On the matching closer (depth back to zero) split into the inner span and the remainder. 'Nothing' diff --git a/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs b/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs index 7dac0f5..c2b793b 100644 --- a/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs +++ b/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs @@ -16,7 +16,7 @@ import Prelude -- | Block helpers that keep the test cases readable. raw :: Text -> B.Block -raw s = B.RawBlock (B.Format "html") s +raw = B.RawBlock (B.Format "html") para :: Text -> B.Block para s = B.Para [B.Str s] From d4bd5f4ef1e3e10413a7f12dbf11f02dcdb3139b Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:21:06 -0400 Subject: [PATCH 3/8] =?UTF-8?q?refactor(hickey):=20closerTag=20=E2=80=94?= =?UTF-8?q?=20require=20explicit=20'>'=20so=20a=20malformed=20')=20doesn't=20silently=20match?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs | 7 +++++-- test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs index 39a0d48..37e98d1 100644 --- a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs +++ b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs @@ -83,8 +83,11 @@ closerTag tag = \case B.RawBlock (B.Format "html") s -> case T.stripPrefix " let (name, after) = T.span isTagNameChar rest - afterClose = T.drop 1 (T.dropWhile (/= '>') after) - in T.toLower name == tag && T.all isSpace afterClose + -- Mirror 'parseOpener': require an explicit @\>@ rather than + -- silently skipping a missing one via @T.drop 1@. Without this, + -- a malformed @\@) would still match. + afterClose = T.stripPrefix ">" (T.dropWhile (/= '>') after) + in T.toLower name == tag && maybe False (T.all isSpace) afterClose Nothing -> False _ -> False diff --git a/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs b/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs index c2b793b..b33880f 100644 --- a/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs +++ b/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs @@ -131,3 +131,9 @@ spec = describe "groupRawHtmlBlocks (srid/emanote#433)" $ do -- An
downstream stays open. let input = [raw "
\n"] groupRawHtmlBlocks input `shouldBe` input + + it "rejects a malformed closer that is missing its '>'" $ do + -- @\@ must not be treated as a valid closer. + -- Mirrors the opener's @T.stripPrefix \">\"@ guard. + let input = [raw "
\n", para "x", raw " Date: Tue, 28 Apr 2026 21:22:59 -0400 Subject: [PATCH 4/8] refactor(hickey+lowy): hoist tag-directive helpers into Render.Internal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "tag" attribute on a B.Div is a directive used by groupRawHtmlBlocks (in RawHtmlGroup) to override the rendered element name. The directive's key was scattered as a string literal across three sites — the producer in RawHtmlGroup and two consumers (getTag, dropTagAttr) buried in a where-clause inside rpBlock'. Promote the protocol to first-class names in Render.Internal: - tagDirectiveKey :: Text — the single source of truth for the key. - divTag :: Text -> B.Attr -> Text — formerly the local getTag. - stripTagDirective :: B.Attr -> B.Attr — formerly the local dropTagAttr. RawHtmlGroup now imports tagDirectiveKey when constructing the Div, and the Div arm of rpBlock' calls divTag/stripTagDirective directly. The where-clause in rpBlock' loses two helpers and Map import; everything else is unchanged. Addresses Hickey #2 (named protocol over implicit convention) and Lowy #2/#4 (extract tagDirectiveKey + named helper for dropTagAttr). --- .../Extra/Splices/Pandoc/RawHtmlGroup.hs | 3 +- src/Heist/Extra/Splices/Pandoc/Render.hs | 13 ++------- .../Extra/Splices/Pandoc/Render/Internal.hs | 28 +++++++++++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs index 37e98d1..51cbe45 100644 --- a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs +++ b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs @@ -43,6 +43,7 @@ module Heist.Extra.Splices.Pandoc.RawHtmlGroup ( import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace) import Data.Text qualified as T +import Heist.Extra.Splices.Pandoc.Render.Internal (tagDirectiveKey) import Text.Pandoc.Definition qualified as B groupRawHtmlBlocks :: [B.Block] -> [B.Block] @@ -51,7 +52,7 @@ groupRawHtmlBlocks = \case b : rest | Just tag <- openerTag b , Just (inner, after) <- splitAtMatchingCloser tag rest -> - B.Div ("", [], [("tag", tag)]) (groupRawHtmlBlocks inner) + B.Div ("", [], [(tagDirectiveKey, tag)]) (groupRawHtmlBlocks inner) : groupRawHtmlBlocks after | otherwise -> b : groupRawHtmlBlocks rest diff --git a/src/Heist/Extra/Splices/Pandoc/Render.hs b/src/Heist/Extra/Splices/Pandoc/Render.hs index 5ba9a9b..f3356b9 100644 --- a/src/Heist/Extra/Splices/Pandoc/Render.hs +++ b/src/Heist/Extra/Splices/Pandoc/Render.hs @@ -11,7 +11,6 @@ module Heist.Extra.Splices.Pandoc.Render ( rawNode, ) where -import Data.Map.Strict qualified as Map import Data.Map.Syntax ((##)) import Data.Text qualified as T import Heist qualified as H @@ -30,7 +29,9 @@ import Heist.Extra.Splices.Pandoc.Render.Internal ( cellColumnIndices, cellSpanAttrs, colSpecsToColgroup, + divTag, mergeStyleKVs, + stripTagDirective, ) import Heist.Extra.Splices.Pandoc.Skylighting (highlightCode) import Heist.Extra.Splices.Pandoc.TaskList qualified as TaskList @@ -151,20 +152,12 @@ rpBlock' ctx@RenderCtx {..} b = case b of tfoot <- wrapSection "tfoot" "td" frows pure $ cg <> thead <> tbody <> tfoot B.Div attr bs -> - one . X.Element (getTag "div" attr) (rpAttr $ rewriteClass ctx (dropTagAttr attr)) + one . X.Element (divTag "div" attr) (rpAttr $ rewriteClass ctx (stripTagDirective attr)) <$> foldMapM (rpBlock ctx) bs B.Figure attr _caption bs -> -- TODO: support caption one . X.Element "figure" (rpAttr attr) <$> foldMapM (rpBlock ctx) bs where - getTag defaultTag (_, _, Map.fromList -> attrs) = - Map.lookup "tag" attrs & fromMaybe defaultTag - - -- The "tag" entry is a directive picked up by 'getTag' to override the - -- element name; it must not survive into the rendered HTML as a literal - -- @tag="…"@ attribute. - dropTagAttr (i, cs, kvs) = (i, cs, filter ((/= "tag") . fst) kvs) - headerSplices headerId innerSplice = do "header:id" ## HI.textSplice headerId "inlines" ## innerSplice diff --git a/src/Heist/Extra/Splices/Pandoc/Render/Internal.hs b/src/Heist/Extra/Splices/Pandoc/Render/Internal.hs index c94b68b..3d5d887 100644 --- a/src/Heist/Extra/Splices/Pandoc/Render/Internal.hs +++ b/src/Heist/Extra/Splices/Pandoc/Render/Internal.hs @@ -9,9 +9,15 @@ module Heist.Extra.Splices.Pandoc.Render.Internal ( cellSpanAttrs, cellColumnIndices, mergeStyleKVs, + + -- * The @"tag"@ directive on 'B.Div' + tagDirectiveKey, + divTag, + stripTagDirective, ) where import Data.List (partition) +import Data.Map.Strict qualified as Map import Data.Text qualified as T import Numeric (showFFloat) import Text.Pandoc.Definition qualified as B @@ -71,3 +77,25 @@ mergeStyleKVs left right = [] -> rest [single] -> rest <> [single] multiple -> rest <> [("style", T.intercalate "; " (snd <$> multiple))] + +{- | The single attribute key that callers (today: 'Heist.Extra.Splices.Pandoc.RawHtmlGroup') +use as a directive on a 'B.Div' to override the rendered element name. The +renderer reads it via 'divTag' and removes it via 'stripTagDirective' so +the directive never leaks into serialised HTML as a literal @tag="…"@. +A named constant rather than three string literals scattered across +modules: future maintainers can grep one place. +-} +tagDirectiveKey :: Text +tagDirectiveKey = "tag" + +{- | Resolve the element name a 'B.Div' should render as: the directive value if + present, else the supplied default. +-} +divTag :: Text -> B.Attr -> Text +divTag defaultTag (_, _, kvs) = + fromMaybe defaultTag (Map.lookup tagDirectiveKey (Map.fromList kvs)) + +-- | Drop the directive from a 'B.Attr' so it doesn't survive into rendered HTML. +stripTagDirective :: B.Attr -> B.Attr +stripTagDirective (i, cs, kvs) = + (i, cs, filter ((/= tagDirectiveKey) . fst) kvs) From e5a71f2870c28280cb84fe9277973998394b8bfc Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:23:39 -0400 Subject: [PATCH 5/8] =?UTF-8?q?docs(lowy):=20RawHtmlGroup=20header=20?= =?UTF-8?q?=E2=80=94=20volatility=20axis=20+=20internal-API=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extra/Splices/Pandoc/RawHtmlGroup.hs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs index 51cbe45..ed32bdb 100644 --- a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs +++ b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs @@ -36,6 +36,26 @@ deliberate scope until a real case demands otherwise. A tag like @\foo\@ that already balances inside one 'B.RawBlock' is left alone — the renderer handles balanced raw-HTML fragments correctly today. + +== Volatility & boundary + +This module exists to encapsulate one specific axis of change: the +strategy for rebalancing orphan raw-HTML blocks around CommonMark "type 6" +content. Future evolution (alternative wrapping strategies, attribute +preservation on the produced 'B.Div', void-element awareness, support for +new HTML element families, smarter nesting heuristics) lives here so the +renderer's interface stays stable. Treat the boundary as load-bearing — +do not inline @groupRawHtmlBlocks@ into the renderer or scatter pieces +of the parsing logic across other modules. + +== Public surface + +The module is exposed in @heist-extra.cabal@ for downstream tests and +ad-hoc tooling that wants to drive the same preprocessing without going +through 'Heist.Extra.Splices.Pandoc.Render.renderPandocWith'. It is not +covered by the library's stability guarantees; the API can change +between minor versions without a deprecation cycle. Mirrors the +arrangement of "Heist.Extra.Splices.Pandoc.Render.Internal". -} module Heist.Extra.Splices.Pandoc.RawHtmlGroup ( groupRawHtmlBlocks, From e3f76880d2430fb0611c24fa8068db4e2d69db99 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:31:09 -0400 Subject: [PATCH 6/8] =?UTF-8?q?refactor(police):=20elegance=20=E2=80=94=20?= =?UTF-8?q?extract=20shared=20parseTagAfterPrefix=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openerTag and closerTag both stripped a prefix, parsed a tag-name span, and verified that nothing but whitespace followed the closing '>'. The opener has one extra check (reject self-closing); other than that the two parsers were the same shape. Extract the shared work into one parseTagAfterPrefix and let openerTag layer the void-element rejection on top. --- .../Extra/Splices/Pandoc/RawHtmlGroup.hs | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs index ed32bdb..c5adc31 100644 --- a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs +++ b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs @@ -79,39 +79,36 @@ groupRawHtmlBlocks = \case -- | Read @b@ as an opening raw-HTML tag, returning the (lowercased) tag name. openerTag :: B.Block -> Maybe Text openerTag = \case - B.RawBlock (B.Format "html") s -> parseOpener (T.strip s) + B.RawBlock (B.Format "html") s -> do + name <- parseTagAfterPrefix "<" s + -- Reject self-closing forms (@\
@): if the body between name and + -- '>' ends in '/', it's a void-element shorthand, not an opener. + let inside = T.takeWhile (/= '>') (T.drop (T.length name + 1) (T.strip s)) + guard $ not ("/" `T.isSuffixOf` T.stripEnd inside) + pure name _ -> Nothing - where - parseOpener s = do - afterLT <- T.stripPrefix "<" s - guard $ not ("/" `T.isPrefixOf` afterLT) - let (name, rest) = T.span isTagNameChar afterLT - guard $ not (T.null name) - -- Reject self-closing forms (@\
@): the last non-space char - -- before '>' is '/'. - let inside = T.takeWhile (/= '>') rest - guard $ not ("/" `T.isSuffixOf` T.stripEnd inside) - -- The opener must be the only thing on this raw block; if anything - -- non-whitespace follows the '>', we're looking at a single-block - -- balanced fragment and must not group it. - afterGT <- T.stripPrefix ">" (T.dropWhile (/= '>') rest) - guard $ T.all isSpace afterGT - pure $ T.toLower name -- | Read @b@ as a closing tag for @tag@. closerTag :: Text -> B.Block -> Bool closerTag tag = \case - B.RawBlock (B.Format "html") s -> case T.stripPrefix " - let (name, after) = T.span isTagNameChar rest - -- Mirror 'parseOpener': require an explicit @\>@ rather than - -- silently skipping a missing one via @T.drop 1@. Without this, - -- a malformed @\@) would still match. - afterClose = T.stripPrefix ">" (T.dropWhile (/= '>') after) - in T.toLower name == tag && maybe False (T.all isSpace) afterClose - Nothing -> False + B.RawBlock (B.Format "html") s -> parseTagAfterPrefix " False +{- | Shared parser for the bare-tag wire format both opener and closer follow: +@PREFIX@ + tag-name chars + optional attribute body + @\>@, with whitespace +the only thing allowed past the closing @\>@. Returns the lowercased tag +name, or 'Nothing' if any guard fails (no @\>@, malformed name, content +past the @\>@). +-} +parseTagAfterPrefix :: Text -> Text -> Maybe Text +parseTagAfterPrefix prefix s = do + body <- T.stripPrefix prefix (T.strip s) + let (name, rest) = T.span isTagNameChar body + guard $ not (T.null name) + afterGT <- T.stripPrefix ">" (T.dropWhile (/= '>') rest) + guard $ T.all isSpace afterGT + pure $ T.toLower name + isTagNameChar :: Char -> Bool isTagNameChar c = isAsciiLower c || isAsciiUpper c || isDigit c || c == '-' From 7677be6f88b7b091a8c21f14d65ff02adbd927b1 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:32:45 -0400 Subject: [PATCH 7/8] refactor(police): rehome tag-directive helpers in RawHtmlGroup, drop unused defaultTag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tag-directive scheme (key + resolver + stripper) was sitting in Render.Internal — a module whose docstring scopes it to "pure helpers extracted from Render.hs", i.e. table rendering. The producer of the directive is RawHtmlGroup, and the volatility lives there: any future change to the wire format starts at the module that decides what shape to emit. Move the three helpers to RawHtmlGroup (the producer) and have Render import from there. Render.Internal is back to its original table-helpers scope. While moving, also drop the unused defaultTag parameter on divTag — every call site passes "div"; bake it in. Switch divTag from Map.fromList+Map.lookup to plain Data.List.lookup since attr lists are flat assoc lists with no duplicates in practice (no behaviour change for real input). Save a Map allocation per Div on the rendering hot path. --- .../Extra/Splices/Pandoc/RawHtmlGroup.hs | 25 ++++++++++++++++- src/Heist/Extra/Splices/Pandoc/Render.hs | 10 ++++--- .../Extra/Splices/Pandoc/Render/Internal.hs | 28 ------------------- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs index c5adc31..7e2ec2f 100644 --- a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs +++ b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs @@ -59,11 +59,20 @@ arrangement of "Heist.Extra.Splices.Pandoc.Render.Internal". -} module Heist.Extra.Splices.Pandoc.RawHtmlGroup ( groupRawHtmlBlocks, + + -- * The @"tag"@ directive on 'B.Div' + + -- | Wire format the pass produces and the renderer consumes. Owned here + -- (the producer) so a future change to the directive scheme starts at the + -- module that decides what shape to emit. + tagDirectiveKey, + divTag, + stripTagDirective, ) where import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace) +import Data.List (lookup) import Data.Text qualified as T -import Heist.Extra.Splices.Pandoc.Render.Internal (tagDirectiveKey) import Text.Pandoc.Definition qualified as B groupRawHtmlBlocks :: [B.Block] -> [B.Block] @@ -119,6 +128,7 @@ means the opener is orphan at this level. splitAtMatchingCloser :: Text -> [B.Block] -> Maybe ([B.Block], [B.Block]) splitAtMatchingCloser tag = go (1 :: Int) [] where + -- Caller has already consumed the opener, so depth starts at 1. go _ _ [] = Nothing go depth acc (b : bs) | closerTag tag b = @@ -129,3 +139,16 @@ splitAtMatchingCloser tag = go (1 :: Int) [] , t == tag = go (depth + 1) (b : acc) bs | otherwise = go depth (b : acc) bs + +-- | Attribute key on 'B.Div' whose value overrides the rendered element name. +tagDirectiveKey :: Text +tagDirectiveKey = "tag" + +-- | Resolve the element name a 'B.Div' should render as, defaulting to @"div"@. +divTag :: B.Attr -> Text +divTag (_, _, kvs) = fromMaybe "div" (lookup tagDirectiveKey kvs) + +-- | Drop the directive from a 'B.Attr' so it doesn't survive into rendered HTML. +stripTagDirective :: B.Attr -> B.Attr +stripTagDirective (i, cs, kvs) = + (i, cs, filter ((/= tagDirectiveKey) . fst) kvs) diff --git a/src/Heist/Extra/Splices/Pandoc/Render.hs b/src/Heist/Extra/Splices/Pandoc/Render.hs index f3356b9..9218dd8 100644 --- a/src/Heist/Extra/Splices/Pandoc/Render.hs +++ b/src/Heist/Extra/Splices/Pandoc/Render.hs @@ -23,15 +23,17 @@ import Heist.Extra.Splices.Pandoc.Ctx ( RenderFeatures (..), rewriteClass, ) -import Heist.Extra.Splices.Pandoc.RawHtmlGroup (groupRawHtmlBlocks) +import Heist.Extra.Splices.Pandoc.RawHtmlGroup ( + divTag, + groupRawHtmlBlocks, + stripTagDirective, + ) import Heist.Extra.Splices.Pandoc.Render.Internal ( alignmentStyle, cellColumnIndices, cellSpanAttrs, colSpecsToColgroup, - divTag, mergeStyleKVs, - stripTagDirective, ) import Heist.Extra.Splices.Pandoc.Skylighting (highlightCode) import Heist.Extra.Splices.Pandoc.TaskList qualified as TaskList @@ -152,7 +154,7 @@ rpBlock' ctx@RenderCtx {..} b = case b of tfoot <- wrapSection "tfoot" "td" frows pure $ cg <> thead <> tbody <> tfoot B.Div attr bs -> - one . X.Element (divTag "div" attr) (rpAttr $ rewriteClass ctx (stripTagDirective attr)) + one . X.Element (divTag attr) (rpAttr $ rewriteClass ctx (stripTagDirective attr)) <$> foldMapM (rpBlock ctx) bs B.Figure attr _caption bs -> -- TODO: support caption diff --git a/src/Heist/Extra/Splices/Pandoc/Render/Internal.hs b/src/Heist/Extra/Splices/Pandoc/Render/Internal.hs index 3d5d887..c94b68b 100644 --- a/src/Heist/Extra/Splices/Pandoc/Render/Internal.hs +++ b/src/Heist/Extra/Splices/Pandoc/Render/Internal.hs @@ -9,15 +9,9 @@ module Heist.Extra.Splices.Pandoc.Render.Internal ( cellSpanAttrs, cellColumnIndices, mergeStyleKVs, - - -- * The @"tag"@ directive on 'B.Div' - tagDirectiveKey, - divTag, - stripTagDirective, ) where import Data.List (partition) -import Data.Map.Strict qualified as Map import Data.Text qualified as T import Numeric (showFFloat) import Text.Pandoc.Definition qualified as B @@ -77,25 +71,3 @@ mergeStyleKVs left right = [] -> rest [single] -> rest <> [single] multiple -> rest <> [("style", T.intercalate "; " (snd <$> multiple))] - -{- | The single attribute key that callers (today: 'Heist.Extra.Splices.Pandoc.RawHtmlGroup') -use as a directive on a 'B.Div' to override the rendered element name. The -renderer reads it via 'divTag' and removes it via 'stripTagDirective' so -the directive never leaks into serialised HTML as a literal @tag="…"@. -A named constant rather than three string literals scattered across -modules: future maintainers can grep one place. --} -tagDirectiveKey :: Text -tagDirectiveKey = "tag" - -{- | Resolve the element name a 'B.Div' should render as: the directive value if - present, else the supplied default. --} -divTag :: Text -> B.Attr -> Text -divTag defaultTag (_, _, kvs) = - fromMaybe defaultTag (Map.lookup tagDirectiveKey (Map.fromList kvs)) - --- | Drop the directive from a 'B.Attr' so it doesn't survive into rendered HTML. -stripTagDirective :: B.Attr -> B.Attr -stripTagDirective (i, cs, kvs) = - (i, cs, filter ((/= tagDirectiveKey) . fst) kvs) From 3bd7abae96f51f488d3a1108ab1c1dbc47b42ff8 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:33:26 -0400 Subject: [PATCH 8/8] docs(police): clean up stale getTag refs and trim oversized docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RenderSpec: comment said 'getTag' but the helper was renamed to divTag. - RawHtmlGroup module docstring: trim the Public-surface paragraph from three sentences to one — the rest narrated cabal config that is one grep away. - RawHtmlGroupSpec: drop the 'Block helpers' header comment that narrated what the next three lines obviously are. --- src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs | 9 +++------ test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs | 1 - test/Heist/Extra/Splices/Pandoc/RenderSpec.hs | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs index 7e2ec2f..b485891 100644 --- a/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs +++ b/src/Heist/Extra/Splices/Pandoc/RawHtmlGroup.hs @@ -50,12 +50,9 @@ of the parsing logic across other modules. == Public surface -The module is exposed in @heist-extra.cabal@ for downstream tests and -ad-hoc tooling that wants to drive the same preprocessing without going -through 'Heist.Extra.Splices.Pandoc.Render.renderPandocWith'. It is not -covered by the library's stability guarantees; the API can change -between minor versions without a deprecation cycle. Mirrors the -arrangement of "Heist.Extra.Splices.Pandoc.Render.Internal". +Exposed for tests and downstream tooling but not covered by the +library's stability guarantees; the API can change between minor +versions without a deprecation cycle. -} module Heist.Extra.Splices.Pandoc.RawHtmlGroup ( groupRawHtmlBlocks, diff --git a/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs b/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs index b33880f..60ec982 100644 --- a/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs +++ b/test/Heist/Extra/Splices/Pandoc/RawHtmlGroupSpec.hs @@ -14,7 +14,6 @@ import Test.Hspec import Text.Pandoc.Definition qualified as B import Prelude --- | Block helpers that keep the test cases readable. raw :: Text -> B.Block raw = B.RawBlock (B.Format "html") diff --git a/test/Heist/Extra/Splices/Pandoc/RenderSpec.hs b/test/Heist/Extra/Splices/Pandoc/RenderSpec.hs index 2df312f..4b5de32 100644 --- a/test/Heist/Extra/Splices/Pandoc/RenderSpec.hs +++ b/test/Heist/Extra/Splices/Pandoc/RenderSpec.hs @@ -344,7 +344,7 @@ spec = do out `shouldSatisfy` ("cell-class" `BS.isInfixOf`) it "drops the `tag` directive from the rendered Div's attributes" $ \hs -> do - -- `tag` is a directive picked up by getTag to override the element + -- `tag` is a directive picked up by divTag to override the element -- name; it must not survive into the rendered HTML as a literal -- @tag="…"@ attribute. Regression for the fix that landed alongside -- the RawHtmlGroup pass (srid/emanote#433).