-- | Functions to access to the Sc3 Rtf & Html based help systems.
module Sound.Sc3.Common.Help where
import Control.Monad {- base -}
import Data.Char {- base -}
import Data.List {- base -}
import Data.Maybe {- base -}
import System.Environment {- base -}
import System.FilePath {- filepath -}
import System.Process {- process -}
import qualified Data.List.Split as Split {- split -}
import qualified Sound.Sc3.Common.Base.System as Base.System {- hsc3 -}
-- * Rtf
-- | Directory containing Sc3 Rtf help files.
sc3_rtf_help_dir :: IO FilePath
sc3_rtf_help_dir = getEnv "Sc3_Rtf_Help_Dir"
{- | Find (case-insensitively) indicated file at 'sc3_rtf_help_dir'.
Runs the command "find -name" (so Unix only).
> sc3_rtf_find_file "SinOsc.help.rtf"
> sc3_rtf_find_file "lfsaw.help.rtf"
> sc3_rtf_find_file "softClip.rtf"
-}
sc3_rtf_find_file :: FilePath -> IO (Maybe FilePath)
sc3_rtf_find_file fn = do
d <- sc3_rtf_help_dir
r <- System.Process.readProcess "find" [d, "-iname", fn] ""
case lines r of
[] -> return Nothing
[r0] -> return (Just r0)
_ -> error "sc3_rtf_find_file: multiple files?"
-- | 'error' variant.
sc3_rtf_find_file_err :: FilePath -> IO FilePath
sc3_rtf_find_file_err = fmap (fromMaybe (error "sc3_rtf_find_file")) . sc3_rtf_find_file
-- | Run the command unrtf (so UNIX only) to convert an RTF file to a TEXT (.scd) file.
sc3_rtf_to_scd :: FilePath -> FilePath -> IO ()
sc3_rtf_to_scd rtf_fn scd_fn = do
txt <- System.Process.readProcess "unrtf" ["--text", rtf_fn] ""
let delete_trailing_whitespace = reverse . dropWhile isSpace . reverse
tidy = unlines . map delete_trailing_whitespace . drop 4 . lines
writeFile scd_fn (tidy txt)
-- | 'sc3_rtf_to_scd' of 'sc3_rtf_find_file_err', writing output to TMPDIR
sc3_rtf_help_translate :: String -> IO FilePath
sc3_rtf_help_translate nm = do
tmp <- Base.System.get_env_default "TMPDIR" "/tmp"
rtf_fn <- sc3_rtf_find_file_err (nm <.> "*rtf")
let scd_fn = tmp > takeFileName rtf_fn -<.> "scd"
sc3_rtf_to_scd rtf_fn scd_fn
return scd_fn
-- | 'sc3_rtf_help_translate' and run editor.
sc3_rtf_help_scd_open :: (String, [String]) -> String -> IO ()
sc3_rtf_help_scd_open (cmd, arg) nm = do
scd_fn <- sc3_rtf_help_translate nm
System.Process.callProcess cmd (arg ++ [scd_fn])
{- | 'sc3_rtf_help_scd_open' with emacsclient --no-wait.
> sc3_rtf_help_scd_open_emacs "lfsaw"
-}
sc3_rtf_help_scd_open_emacs :: String -> IO ()
sc3_rtf_help_scd_open_emacs = sc3_rtf_help_scd_open ("emacsclient", ["--no-wait"])
-- * Sc-Doc (Html)
-- | Url for online Sc-Doc SuperCollider documentation.
sc3_scdoc_help_url :: String
sc3_scdoc_help_url = "http://doc.sccode.org/"
{- | Read the environment variable @Sc3_ScDoc_Html_Help_Dir@.
The default value is @~\/.local\/share\/SuperCollider/Help@.
-}
sc3_scdoc_help_dir :: IO String
sc3_scdoc_help_dir = do
h <- getEnv "HOME"
let d = h > ".local/share/SuperCollider/Help"
Base.System.get_env_default "Sc3_ScDoc_Html_Help_Dir" d
{- | Path to indicated Sc3 class help file.
>>> sc3_scdoc_help_class "SinOsc"
"Classes/SinOsc.html"
-}
sc3_scdoc_help_class :: String -> String
sc3_scdoc_help_class c = "Classes" > c <.> "html"
{- | Generate path to indicated Sc3 operator help file.
>>> sc3_scdoc_help_operator "+"
"Overviews/Operators.html#+"
-}
sc3_scdoc_help_operator :: String -> FilePath
sc3_scdoc_help_operator = (++) "Overviews/Operators.html#"
{- | Generate path to indicated Sc3 method help.
>>> sc3_scdoc_help_method '*' ("C","m")
"Classes/C.html#*m"
-}
sc3_scdoc_help_method :: Char -> (String, String) -> FilePath
sc3_scdoc_help_method z (c, m) = "Classes" > c <.> "html#" ++ [z] ++ m
{- | Generate path to indicated Sc3 class method help.
>>> sc3_scdoc_help_class_method ("C","m")
"Classes/C.html#*m"
-}
sc3_scdoc_help_class_method :: (String, String) -> FilePath
sc3_scdoc_help_class_method = sc3_scdoc_help_method '*'
{- | Generate path to indicated Sc3 instance method help.
>>> sc3_scdoc_help_instance_method ("C","m")
"Classes/C.html#-m"
-}
sc3_scdoc_help_instance_method :: (String, String) -> FilePath
sc3_scdoc_help_instance_method = sc3_scdoc_help_method '-'
{- | Sc3 help path documenting x.
>>> sc3_scdoc_help_path "Operator.distort"
"Overviews/Operators.html#distort"
>>> sc3_scdoc_help_path "Collection.*fill"
"Classes/Collection.html#*fill"
>>> sc3_scdoc_help_path "Collection.inject"
"Classes/Collection.html#-inject"
>>> sc3_scdoc_help_path "SinOsc"
"Classes/SinOsc.html"
-}
sc3_scdoc_help_path :: String -> String
sc3_scdoc_help_path s = do
case Split.splitOn "." s of
["Operator", m] -> sc3_scdoc_help_operator m
[c, '*' : m] -> sc3_scdoc_help_class_method (c, m)
[c, m] -> sc3_scdoc_help_instance_method (c, m)
_ -> sc3_scdoc_help_class s
{- | Open Sc3 help path, either the local file or the online version.
Use @BROWSER@ or @x-www-browser@.
> Base.System.get_env_default "BROWSER" "x-www-browser"
> sc3_scdoc_help_open True (sc3_scdoc_help_path "SinOsc")
> sc3_scdoc_help_open True (sc3_scdoc_help_path "Collection.*fill")
> sc3_scdoc_help_open False (sc3_scdoc_help_path "Collection.inject")
-}
sc3_scdoc_help_open :: Bool -> String -> IO ()
sc3_scdoc_help_open use_loc p = do
d <- sc3_scdoc_help_dir
b <- Base.System.get_env_default "BROWSER" "x-www-browser"
let u = if use_loc then "file://" ++ (d > p) else sc3_scdoc_help_url ++ p
void (System.Process.rawSystem b [u])
{- | Generate path to indicated Sc3 instance method help.
Adds initial forward slash if not present.
> let r = "Reference/Server-Command-Reference.html#/b_alloc"
> sc3_scdoc_help_server_command_path "b_alloc" == r
-}
sc3_scdoc_help_server_command_path :: String -> FilePath
sc3_scdoc_help_server_command_path c =
let c' = case c of
'/' : _ -> c
_ -> '/' : c
in "Reference/Server-Command-Reference.html" ++ ('#' : c')
{- | 'sc3_scdoc_help_open' of 'sc3_server_command_path'
> sc3_scdoc_help_server_command_open True "s_new"
> sc3_scdoc_help_server_command_open False "/b_allocRead"
-}
sc3_scdoc_help_server_command_open :: Bool -> String -> IO ()
sc3_scdoc_help_server_command_open use_loc =
sc3_scdoc_help_open use_loc
. sc3_scdoc_help_server_command_path
-- * Fragments
-- | Apply function at lines of string.
on_lines_of :: ([String] -> [[String]]) -> String -> [String]
on_lines_of f = map unlines . f . lines
{- | Split text into fragments at empty lines.
Hsc3 (and related projects) write help files as sets of distinct fragments.
Fragments are separated by empty lines.
A line containing the special character sequence ---- indicates the end of the fragments.
>>> on_lines_of split_multiple_fragments ";a\nb\n\n\n;c\nd"
[";a\nb\n",";c\nd\n"]
-}
split_multiple_fragments :: [String] -> [[String]]
split_multiple_fragments = filter (not . null) . Split.splitOn [[]]
{- | The text '----' appearing anywhere in a line indicates the end of the graph fragments.
The text '# ' appearing at the start of a line also indicates the end of the graph fragments.
>>> drop_post_graph_section ["a","b","c","","----d","e","f"]
["a","b","c",""]
>>> drop_post_graph_section ["a","b","c","","----d","# e","","f"]
["a","b","c",""]
-}
drop_post_graph_section :: [String] -> [String]
drop_post_graph_section =
let isEnd x = "----" `isInfixOf` x || "# " `isPrefixOf` x
in takeWhile (not . isEnd)
{- | Some help files are in Markdown format.
These are recognised by examing the first two characters, which must be a '#' and ' '.
-}
is_md_help :: String -> Bool
is_md_help x =
case x of
'#' : ' ' : _ -> True
_ -> False
{- | There are two code block formats in markdown help files.
The first indents the block using a single tab or four spaces.
The second marks the start and end of the block by lines starting with three back ticks (`).
See:
and
-}
data CodeBlockType = IndentedCodeBlock | FencedCodeBlock
deriving (Bounded, Enum, Eq, Read, Show)
-- | Get code blocks from Markdown help file.
md_help_get_code_blocks :: [String] -> [(CodeBlockType, [String])]
md_help_get_code_blocks x =
case x of
[] -> []
"```" : x' ->
let (q, x'') = break (== "```") x'
in (FencedCodeBlock, q) : md_help_get_code_blocks (drop 1 x'')
('\t' : _) : _ ->
let (q, x') = span ((==) ['\t'] . take 1) x
in (IndentedCodeBlock, (map (drop 1) q)) : md_help_get_code_blocks x'
_ : x' ->
md_help_get_code_blocks x'
is_doctest_block :: [String] -> Bool
is_doctest_block =
let f x p = p `isPrefixOf` x
in any (\x -> f x ">>> " || f x ">> ")
md_help_get_tab_indented_code_blocks :: [String] -> [[String]]
md_help_get_tab_indented_code_blocks =
filter (not . is_doctest_block)
. map snd
. filter ((== IndentedCodeBlock) . fst)
. md_help_get_code_blocks
{- | Get indented code blocks from Markdown help file.
>>> s <- readFile "/home/rohan/sw/spl/Help/Reference/AllpassC.help.sl"
>>> is_md_help s
True
>>> let b = md_help_get_fenced_code_blocks (lines s)
>>> length b
3
-}
md_help_get_fenced_code_blocks :: [String] -> [[String]]
md_help_get_fenced_code_blocks =
filter (not . is_doctest_block)
. map snd
. filter ((== FencedCodeBlock) . fst)
. md_help_get_code_blocks
get_help_file_fragments :: String -> [String]
get_help_file_fragments s =
if is_md_help s
then on_lines_of md_help_get_fenced_code_blocks s
else on_lines_of (split_multiple_fragments . drop_post_graph_section) s
-- | Read text fragments from file.
read_file_fragments :: FilePath -> IO [String]
read_file_fragments = fmap get_help_file_fragments . readFile
-- | Read text fragments from set of files.
read_file_set_fragments :: [FilePath] -> IO [String]
read_file_set_fragments = fmap concat . mapM read_file_fragments