dhall-docs-1.0.0: src/Dhall/Docs/CodeRenderer.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-| Contains the logic to render the source code inside a HTML. It also provides
context-sensitive features such as jump-to-definition
-}
module Dhall.Docs.CodeRenderer
( renderCodeWithHyperLinks
, renderCodeSnippet
, ExprType(..)
) where
import Data.Text (Text)
import Data.Void (Void)
import Dhall.Core
( Expr (..)
, File (..)
, FilePrefix (..)
, Import (..)
, ImportHashed (..)
, ImportType (..)
, Scheme (..)
, URL (..)
)
import Dhall.Docs.Util
import Dhall.Src (Src (..))
import Lucid
import qualified Data.Text
import qualified Data.Text.Prettyprint.Doc as Pretty
import qualified Data.Text.Prettyprint.Doc.Render.Text as Pretty.Text
import qualified Dhall.Core as Core
import qualified Dhall.Parser
import qualified Dhall.Pretty
import qualified Lens.Family as Lens
import qualified Text.Megaparsec.Pos as SourcePos
-- $setup
-- >>> :set -XOverloadedStrings
-- >>> import Dhall.Core (Directory (..))
-- | Given a Dhall expression return all imports with their location on the file.
-- Contents are already sorted by 'Src', which allows 'renderAsHtml' to traverse
-- the return of this function to inject anchors
getImports :: Expr Src Import -> [(Src, Import)]
getImports (Note src (Embed a)) = [(src, a)]
getImports expr = concatMap getImports $ Lens.toListOf Core.subExpressions expr
fileAsText :: File -> Text
fileAsText File{..} = foldr (\d acc -> acc <> "/" <> d) "" (Core.components directory)
<> "/" <> file
{-| Given an 'Import', render the contents in an HTML element that will allow
users to jump to another file or domain. The 'Text' argument is the contents
inside the anchor tag
Example:
>>> :set -Wno-missing-fields
>>> let file = File { directory = Directory [], file = ""}
>>> let url = URL { scheme = HTTPS, authority = "google.com", query = Nothing, path = file}
>>> let import_ = Import {importHashed = ImportHashed { importType = Remote url }}
>>> renderImport import_ "link for google"
<a href="https://google.com/" target="_blank">link for google</a>
-}
renderImport :: Import -> Text -> Html ()
renderImport (Import {importHashed = ImportHashed { importType }}) =
case importType of
Remote URL {..} -> a_ [href_ href, target_ "_blank"] . toHtml
where
scheme_ = case scheme of
HTTP -> "http"
HTTPS -> "https"
path_ = fileAsText path
query_ = case query of
Nothing -> ""
Just d -> "?" <> d
-- we don't include the headers here since we treat links to open a file
-- in another tab
href = scheme_ <> "://" <> authority <> path_ <> query_
Local Here file -> a_ [href_ href] . toHtml
where
href = "." <> fileAsText file <> ".html"
Local Parent file -> a_ [href_ href] . toHtml
where
href = ".." <> fileAsText file <> ".html"
_ -> toHtml
-- | Given a Text and the parsed `Expr Src Import` from it, this will render the
-- the source code on HTML with jump-to-definition on URL imports. Use this
-- to render the source code with the same structure (whitespaces, comments,
-- language elements) as the source file
renderCodeWithHyperLinks :: Text -> Expr Src Import -> Html ()
renderCodeWithHyperLinks contents expr = pre_ $ go (1, 1) (Data.Text.lines contents) imports
where
imports = getImports expr
-- we keep the current line, column and consumed text as part of function argument
go :: (Int, Int) -> [Text] -> [(Src, Import)] -> Html ()
go _ textLines [] = mapM_ (\t -> toHtml t >> br_ []) textLines
go (currLine, currCol) currentLines ((Src {..}, import_) : rest) = do
let sourceLine = SourcePos.unPos . SourcePos.sourceLine
let sourceColumn = SourcePos.unPos . SourcePos.sourceColumn
let importStartLine = sourceLine srcStart
let importEndLine = sourceLine srcEnd
let importStartCol = sourceColumn srcStart
let importEndCol = sourceColumn srcEnd
let (prefixLines, restLines) = splitAt (importStartLine - currLine) currentLines
let (importLines, suffixLines) = splitAt (importStartLine - importStartLine + 1) restLines
-- calls to `head` and `last` here should never fail since `importLines`
-- have at least one element
let (firstImportLine, lastImportLine) = (head importLines, last importLines)
let prefixCols = Data.Text.take (importStartCol - currCol) firstImportLine
let suffixCols = Data.Text.drop (importEndCol - currCol) lastImportLine
-- prefix lines and columns
mapM_ (\t -> toHtml t >> br_ []) prefixLines
toHtml prefixCols
-- rendered import
renderImport import_ srcText
-- add a newline if last line of import consumes the remaining line on
-- the original text
if Data.Text.null suffixCols then br_ [] else return ()
let suffix = if Data.Text.null suffixCols then suffixLines else suffixCols : suffixLines
-- move the cursor to next line if no characterse are remaining on the
-- suffix cols, otherwise keep the last line and next char right after
-- the import. This is done to handle properly several imports on the
-- same line
let nextPosition = if Data.Text.null suffixCols then
(importEndLine + 1, 1)
else (importEndLine, importEndCol)
go nextPosition suffix rest
-- | Internal utility to differentiate if a Dhall expr is a type annotation
-- or the whole file
data ExprType = TypeAnnotation | AssertionExample
-- | Renders an AST /fragment/ from the source file AST. Use this only for that
-- purpose. The difference between this and 'renderCodeWithHyperLinks' is because that
-- the extracted fragment's 'SourcePos's need to be re-generated to
-- render them in a better way; just adding whitespace at the beginning of the
-- first line won't render good results.
renderCodeSnippet :: Dhall.Pretty.CharacterSet -> ExprType -> Expr Void Import -> Html ()
renderCodeSnippet characterSet exprType expr = renderCodeWithHyperLinks formattedFile expr'
where
layout = case exprType of
AssertionExample -> Dhall.Pretty.layout
TypeAnnotation -> typeLayout
formattedFile = Pretty.Text.renderStrict
$ layout
$ Dhall.Pretty.prettyCharacterSet characterSet (Core.denote expr)
expr' = case Dhall.Parser.exprFromText "" formattedFile of
Right e -> e
Left _ -> fileAnIssue "A failure has occurred while parsing a formatted file"
typeLayout :: Pretty.Doc ann -> Pretty.SimpleDocStream ann
typeLayout = Pretty.removeTrailingWhitespace . Pretty.layoutSmart opts
where
-- this is done so the type of a dhall file fits in a single line
-- its a safe value, since types in source codes are not that large
opts :: Pretty.LayoutOptions
opts = Pretty.defaultLayoutOptions
{ Pretty.layoutPageWidth =
Pretty.Unbounded
}