orchid 0.0.6 → 0.0.7
raw patch · 23 files changed
+554/−525 lines, 23 filesdep +extensible-exceptionsdep +filestoredep ~salvia
Dependencies added: extensible-exceptions, filestore
Dependency ranges changed: salvia
Files
- orchid.cabal +4/−5
- src/Network/Orchid/Backend/CachingBackend.hs +0/−40
- src/Network/Orchid/Backend/DarcsBackend.hs +0/−165
- src/Network/Orchid/Core/Backend.hs +0/−31
- src/Network/Orchid/Core/Format.hs +8/−5
- src/Network/Orchid/Core/Handler.hs +48/−50
- src/Network/Orchid/Core/Liaison.hs +94/−54
- src/Network/Orchid/Format/Haskell.hs +4/−4
- src/Network/Orchid/Format/History.hs +29/−10
- src/Network/Orchid/Format/Html.hs +4/−3
- src/Network/Orchid/Format/Latex.hs +4/−3
- src/Network/Orchid/Format/Pdf.hs +5/−5
- src/Network/Orchid/Format/Plain.hs +4/−3
- src/Network/Orchid/Format/Xml.hs +4/−3
- src/Network/Orchid/Wiki.hs +2/−1
- src/Text/Document/Core/Type.hs +1/−1
- src/Text/Document/Plugin/Formula.hs +6/−17
- src/Text/Xhtml/Xhtml.hs +3/−2
- viewer/js/app.js +206/−35
- viewer/js/ui.js +14/−1
- viewer/show.html +29/−12
- viewer/style/plugins.css +9/−33
- viewer/style/ui.ccss +76/−42
orchid.cabal view
@@ -1,5 +1,5 @@ Name: orchid-Version: 0.0.6+Version: 0.0.7 Description: Haskell Wiki Library Synopsis: Haskell Wiki Library Category: Network, Web@@ -10,8 +10,9 @@ Build-Type: Simple Build-Depends: base, bytestring,- salvia == 0.0.4,+ salvia == 0.0.5, xml,+ extensible-exceptions, containers, QuickCheck, parsec <= 2.1.0.1,@@ -21,6 +22,7 @@ process, unix, nano-md5,+ filestore, hscolour, encoding, utf8-string,@@ -43,9 +45,6 @@ Text.Document.PluginRegister, Text.Xhtml.Xhtml, Text.Xml.Xml,- Network.Orchid.Backend.CachingBackend,- Network.Orchid.Backend.DarcsBackend,- Network.Orchid.Core.Backend, Network.Orchid.Core.Format, Network.Orchid.Core.Handler, Network.Orchid.Core.Liaison,
− src/Network/Orchid/Backend/CachingBackend.hs
@@ -1,40 +0,0 @@-module Network.Orchid.Backend.CachingBackend (cachingBackend) where--#if (__GLASGOW_HASKELL__ < 609)-import Control.Exception hiding (IOException)-#else-import Control.Exception-#endif--import System.Posix.Files-import System.Directory-import qualified System.IO.UTF8 as U--import Network.Orchid.Core.Backend--#if (__GLASGOW_HASKELL__ < 609)-type IOException = Exception-#endif--cachingBackend :: FilePath -> Backend -> Backend-cachingBackend cache backend = Backend {- store = store backend- , delete = delete backend- , retrieve = cachedRetrieve cache backend- , history = history backend- }---------- backend procedures -----------------------------------------------------cachedRetrieve :: FilePath -> Backend -> FilePath -> Revision -> IO (Maybe String)-cachedRetrieve cache backend file rev = do- hit <- (try $ U.readFile $ concat [cache, file, "?", name rev]) :: IO (Either IOException String)- case hit of- -- Cache hit, use this one.- Right doc -> return $ Just doc- -- No cache hit, forward to backend and store in cache dir.- Left e -> do- doc <- retrieve backend file rev--- maybe (return ()) (U.writeFile $ concat [cache, file, "?", name rev]) doc- return doc-
− src/Network/Orchid/Backend/DarcsBackend.hs
@@ -1,165 +0,0 @@-module Network.Orchid.Backend.DarcsBackend (darcsBackend) where--import Control.Applicative hiding (empty)-import Text.ParserCombinators.Parsec hiding (many, optional, (<|>), try)-import System.Directory (removeFile)-import System.Process (runInteractiveProcess, waitForProcess, runProcess)-import System.Exit-import qualified System.IO.UTF8 as U--#if (__GLASGOW_HASKELL__ < 609)-import Control.Exception hiding (IOException)-#else-import Control.Exception-#endif--import Misc.Commons (splitsWith, eitherToMaybe, trim)-import Network.Protocol.Uri ((/+), normalize)-import Network.Orchid.Core.Backend--#if (__GLASGOW_HASKELL__ < 609)-type IOException = Exception-#endif--type DarcsRepository = FilePath--{--Create a Darcs wiki backend bound to the specified repository location. The-temp directory specfied will be used to store intermediate results from-patching old revisions. This directory can easily be shared with the cache-directory for the `CachingBackend' module.--}--darcsBackend :: FilePath -> FilePath -> Backend-darcsBackend repo temp = Backend {- store = darcsStore repo- , delete = darcsDelete repo- , retrieve = darcsRetrieve repo temp- , history = darcsHistory repo- }---------- store a documents in a new darcs patch ---------------------------------darcsStore :: DarcsRepository -> FilePath -> Revision -> String -> IO ()-darcsStore repo file cha@(Revision _ author name) doc = do-- -- Write the new document to the source file.- U.writeFile (repo /+ file) doc-- -- Add file to repository.- runProcess "darcs" ["add", "--case-ok", file]- (Just repo) Nothing Nothing Nothing Nothing- >>= waitForProcess-- -- Record the changes for this file.- runProcess "darcs" [- "record", file- , "--all"- , "--patch-name=" ++ name- , "--author=" ++ author- ] (Just repo) Nothing Nothing Nothing Nothing- >>= waitForProcess-- return ()---------- delete a document from the darcs repo ----------------------------------darcsDelete :: DarcsRepository -> FilePath -> Revision -> IO ()-darcsDelete repo file cha@(Revision _ author name) = do-- -- Write the new document to the source file.- removeFile (repo /+ file)-- -- Record the changes for this file.- runProcess "darcs" [- "record", file- , "--all"- , "--patch-name=" ++ name- , "--author=" ++ author- ] (Just repo) Nothing Nothing Nothing Nothing- >>= waitForProcess-- -- TODO: cache removal.-- return ()-----------------------------------------------------------------------------------darcsRetrieve :: DarcsRepository -> FilePath -> FilePath -> Revision -> IO (Maybe String)-darcsRetrieve repo temp file rev =- case name rev of- "" -> eitherToMaybe <$> (try (U.readFile (repo /+ file)) :: IO (Either IOException String))- n -> retrieveDiff file repo temp n--retrieveDiff :: FilePath -> FilePath -> FilePath -> String -> IO (Maybe String)-retrieveDiff file repo temp name = do-- -- Load the diff from darcs.- (inp, out, err, pid) <-- runInteractiveProcess "darcs" [- "diff", file- , "--unified"- , "--store-in-memory"- , "--to-patch=^" ++ (escapePatch name) ++ "$"- ]- (Just repo) Nothing -- -- Pipe the diff from darcs straight to patch.- let prev = temp ++ file ++ "?" ++ name- pid' <- runProcess "patch" [- "--unified"- , "--output=" ++ prev- ]- (Just repo) Nothing- (Just out) Nothing Nothing-- -- Wait for both processes in pipe to terminate.- exit' <- waitForProcess pid'- exit <- waitForProcess pid-- -- Return just the document or nothing on failure.- case exit of- ExitSuccess -> Just <$> U.readFile (repo /+ prev)- -- TODO: just to damn ugly.- ExitFailure _ -> removeFile (repo /+ prev) >> return Nothing---- Escape special characters in text that will end up in an XML document.-escapePatch :: String -> String-escapePatch [] = []-escapePatch ('(':xs) = "\\(" ++ escapePatch xs-escapePatch (')':xs) = "\\)" ++ escapePatch xs-escapePatch ('+':xs) = "\\+" ++ escapePatch xs-escapePatch ('.':xs) = "\\." ++ escapePatch xs-escapePatch (x:xs) = x : escapePatch xs---------- request and parse modification history ---------------------------------darcsHistory :: DarcsRepository -> FilePath -> IO (Maybe History)-darcsHistory repo file = do- (inp, out, err, pid) <-- runInteractiveProcess "darcs" ["changes", file] (Just repo) Nothing- s <- pHistory <$> U.hGetContents out- waitForProcess pid- return s--pHistory :: String -> Maybe History-pHistory h = splitsWith "\n\n" h >>= pRevisions . snd--pRevisions :: String -> Maybe History-pRevisions his = do- case splitsWith "\n\n" his of- Nothing -> fmap pure $ pRevision his- Just (c, cs) -> do- c' <- pRevision c- cs' <- pRevisions cs- return (c' : cs')--pRevision :: String -> Maybe Revision-pRevision revision = do- let tmplt = length "Sun Aug 10 19:20:52 CEST 2008"- (inf, body) <- splitsWith "\n" revision- let date = take tmplt inf- author = trim (drop tmplt inf)- name = dropWhile (flip elem " *") $ head $ lines body- return $ Revision date author name-
− src/Network/Orchid/Core/Backend.hs
@@ -1,31 +0,0 @@-module Network.Orchid.Core.Backend where--import Data.List (intercalate)--type Author = String-type Date = String--data Revision =- Revision {- date :: Date- , author :: Author- , name :: String- }--instance Show Revision where- show (Revision d a n) = intercalate "\n" [sd, sa, sn]- where- sd = if null d then "<no date>" else d- sa = if null a then "<no author>" else a- sn = if null n then "<no revision name>" else n--type History = [Revision]--data Backend =- Backend {- store :: FilePath -> Revision -> String -> IO ()- , retrieve :: FilePath -> Revision -> IO (Maybe String)- , delete :: FilePath -> Revision -> IO ()- , history :: FilePath -> IO (Maybe History)- }-
src/Network/Orchid/Core/Format.hs view
@@ -1,9 +1,8 @@ module Network.Orchid.Core.Format where import Data.ByteString.Lazy (ByteString)-+import Data.FileStore (FileStore) import Network.Protocol.Uri (URI)-import Network.Orchid.Core.Backend -- Formats produce proper UTF-8 text or binary docs. (TODO: ascii for tex?) data Output =@@ -13,8 +12,12 @@ -- Wiki format description data type. data WikiFormat = WikiFormat {- extension :: String- , mime :: String- , handler :: Backend -> FilePath -> URI -> String -> IO Output+ postfix :: String+ , mime :: String+ , handler :: FileStore+ -> FilePath -- Working dir.+ -> FilePath -- Document name.+ -> String -- Contents.+ -> IO Output }
src/Network/Orchid/Core/Handler.hs view
@@ -1,74 +1,72 @@ module Network.Orchid.Core.Handler (- hRepository+ FileStoreType (..)+ , hRepository , hViewer , hWiki , hWikiCustomViewer ) where -import Control.Monad.State (lift, gets) import Control.Concurrent.STM-+import Control.Monad.State (lift, gets)+import Data.Record.Label+import Misc.Commons ((.$))+import Data.FileStore (FileStore (..), darcsFileStore, gitFileStore)+import Network.Orchid.Core.Format (postfix)+import Network.Orchid.Core.Liaison (hWikiStore, hWikiDeleteOrRename, hWikiRetrieve, hWikiSearch)+import Network.Orchid.FormatRegister (wikiFormats)+import Network.Protocol.Http (Method (..), Status (..), uri)+import Network.Protocol.Uri ((/+), URI(..), path) import Network.Salvia.Advanced.ExtendedFileSystem (hExtendedFileSystem)-import Network.Salvia.Handlers.Directory (hDirectory)+import Network.Salvia.Handlers.Directory (hDirectory, hDirectoryResource) import Network.Salvia.Handlers.Error (hError) import Network.Salvia.Handlers.File (hFileResource) import Network.Salvia.Handlers.File (hUri) import Network.Salvia.Handlers.FileSystem (hFileSystem, hFileTypeDispatcher) import Network.Salvia.Handlers.Login-import Network.Salvia.Handlers.Login (UserDatabase, TUserSession) import Network.Salvia.Handlers.MethodRouter (hPOST, hMethodRouter) import Network.Salvia.Handlers.PathRouter (hPrefix, hPath, hPathRouter) import Network.Salvia.Handlers.Rewrite (hWithoutDir) import Network.Salvia.Httpd (Handler, request)-import Network.Protocol.Http (Method (..), Status (..))-import Network.Protocol.Uri ((/+), URI(..))-import Misc.Commons ((.$))-import Network.Orchid.Backend.CachingBackend (cachingBackend)-import Network.Orchid.Backend.DarcsBackend (darcsBackend)-import Network.Orchid.Core.Backend (Backend)-import Network.Orchid.Core.Format (extension)-import Network.Orchid.Core.Liaison (hWikiStore, hWikiDelete, hWikiRetrieve)-import Network.Orchid.FormatRegister (wikiFormats)- import Paths_orchid -------- main entry point ----------------------------------------------------- -backend :: FilePath -> Backend-backend dataDir =- cachingBackend (dataDir /+ "_cache/")- $ darcsBackend dataDir "_cache/"+data FileStoreType = Git | Darcs -hRepository :: Show a => FilePath -> FilePath -> UserDatabase b -> TUserSession a -> Handler ()-hRepository dataDir workDir userdb session =- hPrefix "/_" - .$ hFileSystem (dataDir /+ "_")- $ hFileTypeDispatcher dataDir- hDirectory- $ hWithoutDir dataDir- $ hWikiREST workDir userdb session- $ backend dataDir+mkFileStore :: FileStoreType -> FilePath -> FileStore+mkFileStore Git = gitFileStore+mkFileStore Darcs = darcsFileStore +-- todo: clean up this mess:+hRepository :: Show a => FileStoreType -> FilePath -> FilePath -> UserDatabase b -> TUserSession a -> Handler ()+hRepository kind repo workDir userdb session =+ let fs = mkFileStore kind repo in+ hPath "/search" (hAuthorized userdb "search" (\_ -> hPOST $ hWikiSearch fs) session)+ $ hPrefix "/_" (hFileSystem (repo /+ "_"))+ $ hFileTypeDispatcher repo hDirectoryResource+ $ const+ $ hWithoutDir repo+ $ hWikiREST workDir userdb session fs+ hViewer :: FilePath -> Handler () hViewer dir = do hPath "/" .$ hFileResource (dir /+ "show.html") $ hExtendedFileSystem dir -hWiki :: Show a => FilePath -> FilePath -> TUserDatabase FilePath -> TUserSession a -> Handler ()-hWiki dataDir workDir userdb session = do+hWiki :: Show a => FileStoreType -> FilePath -> FilePath -> TUserDatabase FilePath -> TUserSession a -> Handler ()+hWiki kind repo workDir userdb session = do viewerDir <- lift $ getDataFileName "viewer"- hWikiCustomViewer viewerDir dataDir workDir userdb session+ hWikiCustomViewer viewerDir kind repo workDir userdb session hWikiCustomViewer :: Show a- => FilePath -> FilePath -> FilePath+ => FilePath -> FileStoreType -> FilePath -> FilePath -> TUserDatabase FilePath -> TUserSession a -> Handler ()-hWikiCustomViewer viewerDir dataDir workDir tuserdb session = do+hWikiCustomViewer viewerDir kind repo workDir tuserdb session = do userdb <- lift . atomically $ readTVar tuserdb hPrefix "/data"- .$ hRepository dataDir workDir userdb session- $ authHandlers tuserdb session- $ hViewer viewerDir+ (hRepository kind repo workDir userdb session)+ (authHandlers tuserdb session $ hViewer viewerDir) authHandlers :: Show a => TUserDatabase FilePath -> TUserSession a -> Handler () -> Handler () authHandlers tuserdb session handler = do@@ -85,23 +83,23 @@ -- The wiki module will act as a REST interface by using the MethodRouter -- handler to dispatch on the HTTP request method. -hWikiREST :: Show a => FilePath -> UserDatabase b -> TUserSession a -> Backend -> Handler ()-hWikiREST workDir userdb session backend =+hWikiREST :: Show a => FilePath -> UserDatabase b -> TUserSession a -> FileStore -> Handler ()+hWikiREST workDir userdb session filestore = hUri $ \uri ->- previewHandlers backend workDir uri- $ actionHandlers backend workDir uri userdb session- $ hError BadRequest+ previewHandlers filestore workDir uri+ $ actionHandlers filestore workDir uri userdb session+ $ hError BadRequest -actionHandlers :: Show a => Backend -> FilePath -> URI -> UserDatabase b -> TUserSession a -> Handler () -> Handler ()-actionHandlers backend workDir uri userdb session =+actionHandlers :: Show a => FileStore -> FilePath -> URI -> UserDatabase b -> TUserSession a -> Handler () -> Handler ()+actionHandlers filestore workDir uri userdb session = hMethodRouter [- (GET, hAuthorized userdb "show" (const $ hWikiRetrieve backend workDir False uri) session)- , (PUT, hAuthorized userdb "edit" (flip (hWikiStore backend) uri) session)- , (DELETE, hAuthorized userdb "delete" (flip (hWikiDelete backend) uri) session)+ (GET, hAuthorized userdb "show" (const $ hWikiRetrieve filestore workDir False uri) session)+ , (PUT, hAuthorizedUser "edit" (flip (hWikiStore filestore) uri) session)+ , (DELETE, hAuthorizedUser "delete" (flip (hWikiDeleteOrRename filestore) uri) session) ] -previewHandlers :: Backend -> FilePath -> URI -> Handler () -> Handler ()-previewHandlers backend workDir uri = hPathRouter (- map (\ext -> ("/preview." ++ ext, hWikiRetrieve backend workDir True uri))- $ map extension wikiFormats) +previewHandlers :: FileStore -> FilePath -> URI -> Handler () -> Handler ()+previewHandlers filestore workDir uri = hPathRouter (+ map (\ext -> ("/preview." ++ ext, hWikiRetrieve filestore workDir True uri))+ $ map postfix wikiFormats)
src/Network/Orchid/Core/Liaison.hs view
@@ -1,29 +1,30 @@ module Network.Orchid.Core.Liaison ( hWikiRetrieve- , hWikiDelete+ , hWikiDeleteOrRename , hWikiStore+ , hWikiSearch ) where import Control.Applicative ((<$>))+import Control.Exception.Extensible (try) import Control.Monad.State (gets, lift) import Data.Encoding (encodeLazy) import Data.Encoding.UTF8 (UTF8 (..))-import Data.List (find)-import qualified Data.ByteString.Lazy as B-+import Data.FileStore hiding (NotFound)+import Data.List (find, intercalate)+import Data.Record.Label+import Network.Orchid.Core.Format (WikiFormat (..), Output (..))+import Network.Orchid.FormatRegister+import Network.Protocol.Http+import Network.Protocol.Uri import Network.Salvia.Handlers.Error (safeIO, hError, hCustomError) import Network.Salvia.Handlers.File ()-import Network.Salvia.Handlers.Login (username, User)+import Network.Salvia.Handlers.Login (User, username, email) import Network.Salvia.Handlers.MethodRouter () import Network.Salvia.Handlers.PathRouter ()-import Network.Salvia.Httpd (UriHandler, sendBs, request, modResponse, contentsUtf8)-import Network.Protocol.Http-import Network.Orchid.Core.Backend (Backend (..), Revision (..))-import Network.Orchid.Core.Format (WikiFormat (..), Output (..))-import Network.Orchid.FormatRegister-import qualified Network.Protocol.Uri as U---- TODO: should history be here instead of format dir?+import Network.Salvia.Httpd (Handler, UriHandler, sendBs, sendStr, request, response, contentsUtf8, uriEncodedPostParamsUTF8)+import Misc.Commons (safeRead)+import qualified Data.ByteString.Lazy as B -------- showing wiki documents ----------------------------------------------- @@ -33,76 +34,115 @@ print this back in the requested format. -} -hWikiRetrieve :: Backend -> FilePath -> Bool -> UriHandler ()-hWikiRetrieve backend workDir b u = do+hWikiRetrieve :: FileStore -> FilePath -> Bool -> UriHandler ()+hWikiRetrieve filestore workDir b u = do - -- Fetch revision identifier from URI query string.- rev <- Revision "" "" . U.query . uri <$> gets request+ -- Fetch revision identifier from URI query string and convert this to a+ -- Maybe based on string-emptyness.+ let rev = lget query u+ revId = if null rev then Nothing else Just rev -- Compute the source file and format handler.- let src = U.relative $ U.setExtension Nothing u- ext = maybe "txt" id $ U.extension u- fmt = maybe defaultFormat id $ find ((ext==) . extension) wikiFormats+ let src = mkPathRelative $ lset extension Nothing $ lget path u+ ext = maybe "txt" id $ lget (extension % path) u+ fmt = maybe defaultFormat id $ find ((ext==) . postfix) wikiFormats - -- The body might be retrieved from our backend or from the request itself.+ -- The body might be retrieved from our filestore or from the request itself. body <- if b then contentsUtf8- else lift (retrieve backend (U.path src) rev)+ else lift (+ either (\e -> const Nothing (e::FileStoreError)) Just + <$> try (smartRetrieve filestore False src revId)) -- Format the body using the selected wiki handler or return an error when -- the body could not be retrieved. case body of Nothing -> hError NotFound Just s -> do- b <- lift $ (handler fmt) backend workDir src s+ b <- lift $ (handler fmt) filestore workDir src s (body, enc) <- return $ case b of TextOutput s -> (encodeLazy UTF8 s, Just utf8) BinaryOutput bs -> (bs, Nothing) - modResponse- $ setStatus OK- . setContentType (mime fmt) enc- . setContentLength (B.length body)+ enterM response $ do+ setM status OK+ setM contentType (mime fmt, enc)+ setM contentLength (Just $ fromIntegral $ B.length body) sendBs body -------- deleting wiki documents ---------------------------------------------- -- TODO:generalize deletion/storage -hWikiDelete :: Backend -> Maybe User -> UriHandler ()-hWikiDelete backend user u = do+hWikiDeleteOrRename :: FileStore -> User -> UriHandler ()+hWikiDeleteOrRename filestore user u = do - -- Fetch revision identifier from URI query string.- revname <- gets (U.query . uri . request)- let rev = Revision "" (maybe "guest" username user) revname- let src = U.setExtension Nothing u+ let rev = lget query u - safeIO- (delete backend (U.path $ U.relative src) rev)- (const $ return ())+ if null rev+ then hCustomError BadRequest errEmptyRev+ else do+ mdoc <- contentsUtf8+ let aut = Author (username user) (email user)+ src = mkPathRelative $ lset extension Nothing $ lget path u+ lift $ case mdoc of+ Nothing -> delete filestore src aut rev+ Just mv -> rename filestore src mv aut rev +errEmptyRev, errEmptyRes :: String+errEmptyRev = "empty revision name not allowed"+errEmptyRes = "empty resource name not allowed"+ -------- storing wiki documents ----------------------------------------------- -hWikiStore :: Backend -> Maybe User -> UriHandler ()-hWikiStore backend user u = do+hWikiStore :: FileStore -> User -> UriHandler ()+hWikiStore filestore user u = do - -- Fetch revision identifier from URI query string.- revname <- gets (U.query . uri . request)- let rev = Revision "" (maybe "guest" username user) revname+ let rev = lget query u+ mdoc <- contentsUtf8+ case (null rev, mdoc) of - let src = U.setExtension Nothing u+ -- Error cases.+ (True, Nothing) -> hCustomError BadRequest (errEmptyRev ++ "\n" ++ errEmptyRes)+ (True, Just _) -> hCustomError BadRequest errEmptyRev+ (False, Nothing) -> hCustomError BadRequest errEmptyRes - -- Check history to prevent duplicate revision name.- p <- maybe [] (filter (\(Revision d a n) -> n == revname))- <$> lift (history backend (U.path $ U.relative src))+ (False, Just doc) -> lift $ do+ let aut = Author (username user) (email user)+ src = mkPathRelative $ lset extension Nothing $ lget path u+ save filestore src aut rev doc - case p of- x:xs -> hCustomError BadRequest "existing revision name"- [] -> do- c <- contentsUtf8- case c of- Nothing -> modResponse $ setStatus BadRequest- Just c' -> safeIO- (store backend (U.path $ U.relative src) rev c')- (const $ return ())+-------- searching wiki documents ---------------------------------------------++hWikiSearch :: FileStore -> Handler ()+hWikiSearch filestore = do++ params <- uriEncodedPostParamsUTF8++ case getSearchInfo params of+ Nothing -> hCustomError BadRequest "no search query specified"+ Just (a, b, c, d) -> do+ res <- lift $ search filestore (SearchQuery [a] b c d)+ enterM response $ do+ setM status OK+ setM contentType ("text/plain", Just utf8)+ sendStr (intercalate "\n\n" $ map showMatch res)++getSearchInfo :: Maybe Parameters -> Maybe (String, Bool, Bool, Bool)+getSearchInfo params = do+ p <- params+ patterns <- "patterns" `lookup` p >>= id+ wholewords <- "wholewords" `lookup` p >>= id >>= safeRead+ matchall <- "matchall" `lookup` p >>= id >>= safeRead+ ignorecase <- "ignorecase" `lookup` p >>= id >>= safeRead+ return (patterns, wholewords, matchall, ignorecase)++showMatch :: SearchMatch -> String+showMatch match =+ intercalate "\n"+ $ map (\(a, b) -> a ++ "=" ++ b match) [+ ("resource", matchResourceName)+ , ("linenumber", show . matchLineNumber)+ , ("line", matchLine)+ ]
src/Network/Orchid/Format/Haskell.hs view
@@ -1,14 +1,14 @@ module Network.Orchid.Format.Haskell (fHaskell) where -import Text.Document.Document-import Network.Orchid.Core.Backend+import Data.FileStore (FileStore) import Network.Orchid.Core.Format import Network.Protocol.Uri+import Text.Document.Document fHaskell = WikiFormat "hs" "text/plain" haskell -haskell :: Backend -> FilePath -> URI -> String -> IO Output-haskell backend _ _ src = return +haskell :: FileStore -> FilePath -> FilePath -> String -> IO Output+haskell _ _ _ src = return $ TextOutput $ either show show $ fromWiki src
src/Network/Orchid/Format/History.hs view
@@ -1,19 +1,38 @@ module Network.Orchid.Format.History (fHistory) where import Control.Monad (liftM)+import Data.FileStore (FileStore (history), Author (..), TimeRange (..), Revision (..)) import Data.List (intercalate)+import Data.Record.Label (lget)+import Network.Orchid.Core.Format (Output (..), WikiFormat (..))+import Network.Protocol.Uri (URI, path, mkPathRelative)+import Text.Document.Document () -import Text.Document.Document-import Network.Orchid.Core.Backend-import Network.Orchid.Core.Format-import qualified Network.Protocol.Uri as U+fHistory :: WikiFormat+fHistory = WikiFormat "his" "text/plain" his -fHistory = WikiFormat "his" "text/plain" history'+his :: FileStore -> FilePath -> FilePath -> String -> IO Output+his filestore _ p _ = do -history' :: Backend -> FilePath -> U.URI -> String -> IO Output-history' backend _ f _ = do- liftM- (TextOutput . maybe "no history" (intercalate "\n\n" . map show))- (history backend $ U.path $ U.relative f)+ -- Get all the history for one single document.+ revs <- history filestore [mkPathRelative p] noTimeRange+ return+ $ TextOutput+ $ intercalate "\n\n"+ $ map showRevision+ revs +noTimeRange :: TimeRange+noTimeRange = TimeRange Nothing Nothing++showRevision :: Revision -> String+showRevision rev =+ intercalate "\n"+ $ map (\(a, b) -> a ++ "=" ++ b rev) [+ ("date", show . revDateTime)+ , ("author", authorName . revAuthor)+ , ("email", authorEmail . revAuthor)+ , ("description", revDescription)+ , ("id", revId)+ ]
src/Network/Orchid/Format/Html.hs view
@@ -2,14 +2,15 @@ import Control.Monad (liftM) -import Text.Document.Document-import Network.Orchid.Core.Backend+import Data.FileStore (FileStore) import Network.Orchid.Core.Format import Network.Protocol.Uri+import Text.Document.Document +fHtml :: WikiFormat fHtml = WikiFormat "html" "text/html" html -html :: Backend -> FilePath -> URI -> String -> IO Output+html :: FileStore -> FilePath -> FilePath -> String -> IO Output html _ work _ src = do case fromWiki src of Left err -> return $ TextOutput (show err)
src/Network/Orchid/Format/Latex.hs view
@@ -1,13 +1,14 @@ module Network.Orchid.Format.Latex (fLatex) where -import Text.Document.Document-import Network.Orchid.Core.Backend+import Data.FileStore (FileStore) import Network.Orchid.Core.Format import Network.Protocol.Uri+import Text.Document.Document +fLatex :: WikiFormat fLatex = WikiFormat "tex" "text/plain" latex -latex :: Backend -> FilePath -> URI -> String -> IO Output+latex :: FileStore -> FilePath -> FilePath -> String -> IO Output latex _ _ _ src = return $ TextOutput $ either show toLaTeX
src/Network/Orchid/Format/Pdf.hs view
@@ -2,18 +2,18 @@ import Control.Monad (liftM) import Data.ByteString.Lazy hiding (writeFile)+import Data.FileStore (FileStore)+import Network.Orchid.Core.Format+import Network.Protocol.Uri import Prelude hiding (readFile) import System.Process (waitForProcess, runProcess)- import Text.Document.Document-import Network.Protocol.Uri-import Network.Orchid.Core.Backend-import Network.Orchid.Core.Format +fPdf :: WikiFormat fPdf = WikiFormat "pdf" "application/pdf" pdf -- TODO: write to _cache dir, not /tmp-pdf :: Backend -> FilePath -> URI -> String -> IO Output+pdf :: FileStore -> FilePath -> FilePath -> String -> IO Output pdf _ _ _ src = do writeFile "/tmp/tex-to-pdf.tex" $ either show toLaTeX
src/Network/Orchid/Format/Plain.hs view
@@ -1,12 +1,13 @@ module Network.Orchid.Format.Plain (fPlain) where -import Text.Document.Document-import Network.Orchid.Core.Backend+import Data.FileStore (FileStore) import Network.Orchid.Core.Format import Network.Protocol.Uri+import Text.Document.Document +fPlain :: WikiFormat fPlain = WikiFormat "txt" "text/plain" plain -plain :: Backend -> FilePath -> URI -> String -> IO Output+plain :: FileStore -> FilePath -> FilePath -> String -> IO Output plain _ _ _ = return . TextOutput
src/Network/Orchid/Format/Xml.hs view
@@ -2,14 +2,15 @@ import Text.XML.Light.Output -import Text.Document.Document-import Network.Orchid.Core.Backend+import Data.FileStore (FileStore) import Network.Orchid.Core.Format import Network.Protocol.Uri+import Text.Document.Document +fXml :: WikiFormat fXml = WikiFormat "xml" "text/xml" xml -xml :: Backend -> FilePath -> URI -> String -> IO Output+xml :: FileStore -> FilePath -> FilePath -> String -> IO Output xml _ _ _ src = return $ TextOutput $ either show (ppContent . toXML)
src/Network/Orchid/Wiki.hs view
@@ -1,5 +1,6 @@ module Network.Orchid.Wiki (- hRepository+ FileStoreType (..)+ , hRepository , hViewer , hWiki ) where
src/Text/Document/Core/Type.hs view
@@ -394,7 +394,7 @@ _contentsIxml :: (XL.Content) -- "src/Text/Document/Printer/Xhtml.ag"(line 89, column 16) _lhsOxhtml =- nameDiv name_ _contentsIxhtml+ nameAnchor name_ _contentsIxhtml -- "src/Text/Document/Printer/Xml.ag"(line 87, column 16) _lhsOxml = xmlElem "anchor" [xmlText name_, _contentsIxml]
src/Text/Document/Plugin/Formula.hs view
@@ -1,35 +1,24 @@ module Text.Document.Plugin.Formula (formulaPlugin) where import Control.Applicative--#if (__GLASGOW_HASKELL__ < 609)-import Control.Exception hiding (IOException)-#else-import Control.Exception-#endif-+import Control.Exception.Extensible import Control.Monad (when)+import Data.ByteString (pack) import Data.Char (ord) import Data.Digest.OpenSSL.MD5 (md5sum)-import Data.ByteString (pack) import Data.List (intercalate)+import Misc.Commons (trim)+import Network.Protocol.Uri ((/+)) import System.IO import System.IO.Unsafe import System.Posix.Files (getFileStatus, isRegularFile) import System.Process (runInteractiveProcess, waitForProcess, runProcess)-import Text.ParserCombinators.Parsec hiding (many, optional, (<|>), try)--import Network.Protocol.Uri ((/+))-import Text.Document.Parser.WikiHelper import Text.Document.Core.Type+import Text.Document.Parser.WikiHelper import Text.Document.Plugin-import Misc.Commons (trim)+import Text.ParserCombinators.Parsec hiding (many, optional, (<|>), try) import qualified Text.Xhtml.Xhtml as H import qualified Text.Xml.Xml as X--#if (__GLASGOW_HASKELL__ < 609)-type IOException = Exception-#endif -------- hscolour plugin ------------------------------------------------------
src/Text/Xhtml/Xhtml.hs view
@@ -62,6 +62,7 @@ withName n e = attribute "name" n . e -nameDiv = flip withName div-nameSpan = flip withName span+nameAnchor = flip withName (element "a")+nameDiv = flip withName div+nameSpan = flip withName span
viewer/js/app.js view
@@ -6,6 +6,7 @@ setupLoadingIcon() installCollapsers() installOptions()+ installEnters() installSmartFields() installHandlers() index()@@ -59,7 +60,11 @@ $('#create-account-btn').click(signup) $('#save-btn').click(save) $('#preview-btn').click(preview)+ $('#rename-btn').click(rename) $('#delete-btn').click(_delete)+ $('#search-btn').click(search)+ $('#search-prev-btn').click(searchPrev)+ $('#search-next-btn').click(searchNext) } /* ------------------------------------------------------------------------- */@@ -71,6 +76,7 @@ // Static information. prefix : 'data/' , previewURI : 'data/preview'+ , searchURI : 'data/search' , loginURI : 'login' , logoutURI : 'logout' , loginfoURI : 'loginfo'@@ -90,6 +96,12 @@ , author : '' , user : ''++ , search : {+ results : []+ , max : 20+ , index : 0+ } } function parseURI ()@@ -133,13 +145,13 @@ t = 1000 } - if (Wiki.anchor)- setTimeout(- function ()- {+ // if (Wiki.anchor)+ // setTimeout(+ // function ()+ // { followAnchor(Wiki.anchor)- }, t- )+ // }, t+ // ) } function makeURI (prefix, title, ext, rev, anchor)@@ -199,6 +211,7 @@ { viewerHTML(res) $('#history-btn').removeClass('disabled')+ panelClose($('#edit-btn'), $('#editor')) processLinks() processImages() setExternalLinks()@@ -247,28 +260,29 @@ function followAnchor (ref) {- var target = $('*[name=' + ref + ']')[0]+ var target = $('a[name=' + ref + ']')[0] target.scrollIntoView() - setTimeout(- function ()- {- focusElement(target)- }, 0)+ // setTimeout(+ // function ()+ // {+ // focusElement(target.childeNodes[0])+ // }, 0) } -function focusElement (target)-{- var div = document.createElement('div')- document.body.appendChild(div)- var bcr = target.getBoundingClientRect()- div.style.left = bcr.left- div.style.top = bcr.top- div.style.width = target.clientWidth- div.style.height = target.clientHeight- $(div).addClass('find')- $(div).fadeOut('slow')-}+// function focusElement (target)+// {+ // alert(target)+ // var div = document.createElement('div')+ // document.body.appendChild(div)+ // var bcr = target.getBoundingClientRect()+ // div.style.left = bcr.left+ // div.style.top = bcr.top+ // div.style.width = target.clientWidth+ // div.style.height = target.clientHeight+ // $(div).addClass('find')+ // $(div).fadeOut('slow')+// } /* ---- authentication bar ------------------------------------------------- */ @@ -347,6 +361,7 @@ Wiki.signupURI , { username : $('#su-username').val() , password : $('#su-password').val()+ , email : $('#su-email').val() } , function () {@@ -374,7 +389,16 @@ $.get(makeURIExt('his'), '', function (res) {- $('#history').html(historyTable(res))+ $('#history-results').html(historyTable(res))+ $('#history-status').removeClass('error')+ $('#history-status').html('')+ },+ '',+ function (e)+ {+ $('#history-results').html('')+ $('#history-status').addClass('error')+ $('#history-status').html('no history available: <span>' + e.responseText + '</span>') } ) }@@ -383,10 +407,11 @@ { function row (c, i) {- var lines = c.split('\n')- , modified = lines.shift()- , author = lines.shift()- , name = lines.shift()+ var modified = c.match( /(^|[\n])date=([^\n]*)/ )[2]+ var author = c.match( /(^|[\n])author=([^\n]*)/ )[2]+ var email = c.match( /(^|[\n])email=([^\n]*)/ )[2]+ var description = c.match( /(^|[\n])description=([^\n]*)/ )[2]+ var revid = c.match( /(^|[\n])id=([^\n]*)/ )[2] // Side note. var klass =@@ -398,13 +423,14 @@ , '<tr $klass>' , '<td>$modified</td>' , '<td>$author</td>'- , '<td><a href="' + makeURI(Wiki.viewerURI, undefined, undefined, i == 0 ? '' : name) + '">$name</a></td>'+ , '<td><a href="$uri">$description</a></td>' , '</tr>' ].join('').interpolate({- 'klass' : klass- , 'modified' : modified- , 'author' : author- , 'name' : name+ 'uri' : makeURI(Wiki.viewerURI, undefined, undefined, description)+ , 'klass' : klass+ , 'modified' : modified+ , 'author' : author+ , 'description' : description }) } @@ -485,6 +511,20 @@ ) } +function rename ()+{+ $._delete(+ makeURI(undefined, undefined, '', $('#change.full').val())+ , $('#rename').val()+ , function (res)+ {+ $('#edit-status').removeClass('error')+ $('#edit-status').html('rename successful')+ document.location.hash = $('#rename').val()+ }+ )+}+ function _delete () { $._delete(@@ -494,8 +534,139 @@ { $('#edit-status').removeClass('error') $('#edit-status').html('delete successful')- // todo: close edit & history.+ document.location.hash = '' } )+}++/* ---- search bar --------------------------------------------------------- */++function search ()+{+ if ($('#search-btn').hasClass('disabled'))+ return;++ $('#search-status').removeClass('error')+ $('#search-status').html('searching...')++ $.post(+ Wiki.searchURI+ , { patterns : $('#search-terms').val()+ , wholewords : 'False'+ , matchall : 'False'+ , ignorecase : $('#search-case:checked').length == 0 ? 'True' : 'False'+ }+ , function (res)+ {+ var rs = Wiki.search.results = res.asHTML().split("\n\n")+ Wiki.search.index = 0++ if (!rs || rs.length == 0 || (rs.length == 1 && !rs[0])) {+ $('#search-status').addClass('error')+ $('#search-status').html('no search results')+ searchPrevReset()+ searchNextReset()+ $('#search-results').html('')+ } else {+ $('#search-status').removeClass('error')+ $('#search-status').html('search successful (' + rs.length + ' results)')+ $('#search-results').html(searchTable())+ }+ }+ , ''+ , function (e)+ {+ $('#search-status').addClass('error')+ $('#search-status').html('search failed: <span>' + e.responseText + '</span>')+ }+ )++}++function searchPrevReset ()+{+ $('#search-prev-btn').addClass('disabled')+ $('#search-prev-btn').html('previous results')+}++function searchNextReset ()+{+ $('#search-next-btn').addClass('disabled')+ $('#search-next-btn').html('next results')+}++function searchNext ()+{+ if ($('#search-next-btn').hasClass('disabled'))+ return;++ Wiki.search.index += Wiki.search.max+ $('#search-results').html(searchTable())+}++function searchPrev ()+{+ if ($('#search-prev-btn').hasClass('disabled'))+ return;++ Wiki.search.index -= Wiki.search.max+ $('#search-results').html(searchTable())+}++function searchTable ()+{+ function row (c, i)+ {+ var resource = c.match( /(^|[\n])resource=([^\n]*)/ )[2]+ var linenumber = c.match( /(^|[\n])linenumber=([^\n]*)/ )[2]+ var line = c.match( /(^|[\n])line=([^\n]*)/ )[2]++ return [+ '<tr>'+ , '<td><a href="$uri">$resource</a></td>'+ , '<td>$linenumber</td>'+ , '<td>$line</td>'+ , '</tr>'+ ].join('').interpolate({+ 'uri' : makeURI(Wiki.viewerURI, resource, undefined, '')+ , 'resource' : resource+ , 'linenumber' : linenumber+ , 'line' : line+ })+ }++ function col (c)+ {+ return '<td>' + c + '</td>'+ }++ var rs = Wiki.search.results+ ix = Wiki.search.index+ max = Wiki.search.max+ prevIx = Math.max(0, ix - max) + prevMax = Math.max(0, ix) + nextIx = Math.min(rs.length - 1, ix + max)+ nextMax = Math.min(rs.length - 1, ix + max + max)++ if (prevMax > 0) {+ $('#search-prev-btn').removeClass('disabled')+ $('#search-prev-btn').html('previous results <span>(' + prevIx + '-' + prevMax + ')</span>')+ } else+ searchPrevReset()++ if (nextIx < rs.length - 1) {+ $('#search-next-btn').removeClass('disabled')+ $('#search-next-btn').html('next results <span>(' + nextIx + '-' + nextMax + ')</span>')+ } else+ searchNextReset()++ return '' + + '<table cellpadding="0" cellspacing="0"><tr>'+ + '<th id="resource">resource</th>'+ + '<th id="linenumber">line</th>'+ + '<th id="line">match</th>'+ + '</tr>'+ + rs.slice(ix, ix + max).map(row).join('')+ + '</tbody></table>' }
viewer/js/ui.js view
@@ -58,6 +58,18 @@ /* ------------------------------------------------------------------------- */ +function installEnters ()+{+ $('input[enter]').keyup(+ function(e) {+ if(e.keyCode == 13)+ window[$(this).attr('enter')]()+ }+ )+}++/* ------------------------------------------------------------------------- */+ function installSmartFields () { $('input')@@ -115,7 +127,8 @@ function (a) { var elem = $(others[a])- return elem.attr('target').match(targets[t])+ return elem.attr('target')+ && elem.attr('target').match(targets[t]) && !elem.hasClass('full') } )
viewer/show.html view
@@ -45,18 +45,19 @@ <!-- toolbar section --> <div id="toolbar">- <a id="auth-btn" class="collapser" target="#auth">login</a>- <a id="signup-btn" class="collapser" target="#signup">signup</a>- <a id="format-btn" class="collapser" target="#format">format</a>- <a id="history-btn" class="collapser" target="#history">history</a>- <a id="edit-btn" class="collapser" target="#editor">edit</a>+ <a id="auth-btn" class="collapser" target="#auth">login</a>+ <a id="signup-btn" class="collapser" target="#signup">signup</a>+ <a id="format-btn" class="collapser" target="#format">format</a>+ <a id="history-btn" class="collapser" target="#history">history</a>+ <a id="edit-btn" class="collapser" target="#editor">edit</a>+ <a id="searchbar-btn" class="collapser" target="#search">search</a> </div> <!-- auth section --> <div id="auth"> <input id="user-ui" target="#logout-btn, #save-btn, #delete-btn" type="text"></input>- <input name="username" target="#login-btn" id="username" type="text" value="username"></input>- <input name="password" target="#login-btn" id="password" type="password" value="password"></input>+ <input name="username" enter="login" target="#login-btn" id="username" type="text" value="username"></input>+ <input name="password" enter="login" target="#login-btn" id="password" type="password" value="password"></input><br/> <a id="login-btn">login</a> <a id="logout-btn">logout</a> <div id="login-status" class="status"> </div>@@ -64,8 +65,9 @@ <!-- signup section --> <div id="signup">- <input name="username" id="su-username" target="#create-account-btn" type="text" value="username"></input>- <input name="password" id="su-password" target="#create-account-btn" type="password" value="password"></input>+ <input name="username" id="su-username" enter="signup" target="#create-account-btn" type="text" value="username"></input>+ <input name="password" id="su-password" enter="signup" target="#create-account-btn" type="password" value="password"></input>+ <input name="email" id="su-email" enter="signup" target="#create-account-btn" type="text" value="e-mail"></input><br/> <a id="create-account-btn">create account</a> <div id="signup-status" class="status"> </div> </div>@@ -86,17 +88,32 @@ </div> <!-- history section -->- <div id="history"></div>+ <div id="history">+ <div id="history-status" class="status"> </div>+ <div id="history-results"></div>+ </div> <!-- editor section --> <div id="editor"> <textarea id="editField" cols="40"></textarea>- <input id="change" name="change description"- type="text" target="#save-btn, #delete-btn" value="change description"></input>+ <input name="change description" target="#save-btn, #delete-btn, #rename-btn" id="change" type="text" value="change description"></input>+ <input name="document name" target="#rename-btn" id="rename" type="text" value="document name"></input> <a id="save-btn">save</a> <a id="preview-btn">preview</a> <a id="delete-btn">delete</a>+ <a id="rename-btn">rename</a> <div id="edit-status" class="status"> </div>+ </div>++ <!-- search section -->+ <div id="search">+ <input name="search terms" enter="search" target="#search-btn" id="search-terms" type="text" value="search-terms"></input>+ <input type="checkbox" class="checkbox" id="search-case" value="no">case sensitive</input><br>+ <a id="search-btn">search</a>+ <a id="search-prev-btn" class="disabled">previous results</a>+ <a id="search-next-btn" class="disabled">next results</a>+ <div id="search-status" class="status"> </div>+ <div id="search-results"></div> </div> </div>
viewer/style/plugins.css view
@@ -8,39 +8,15 @@ margin-bottom : 1em; } -.varop,-.keyglyph {- color : #960;-}--.definition {- color : #005;- font-weight : bold;-}--.varid {- color : #444;-}--.keyword {- font-weight : bold;-}--.comment {- color : #44f;-}--.conid {- color : #000;-}--.num {- color : #00a;-}--.str {- color : #a00;-}+.varop { color : #960; font-weight : normal; }+.keyglyph { color : #960; font-weight : normal; }+.definition { color : #005; font-weight : bold; }+.varid { color : #444; font-weight : normal; }+.keyword { color : #000; font-weight : bold; }+.comment { color : #44f; font-weight : normal; }+.conid { color : #000; font-weight : normal; }+.num { color : #00a; font-weight : normal; }+.str { color : #a00; font-weight : normal; } /* ---- latex formula ------------------------------------------------------ */
viewer/style/ui.ccss view
@@ -28,13 +28,53 @@ border : solid 8px #a47 position : fixed -#format, #auth, #signup, #history, #editor:+#format, #auth, #signup, #history, #editor, #search: display : none #ui: + table:+ width : 100%+ font-size : 0.7em+ line-height : 1.3em++ td, th:+ padding : 0.4em+ padding-left : 2em+ vertical-align : top+ font-size : 1.1em++ .current td:+ background : #fff++ td:+ background : #eee+ text-align : left+ border-top : solid 1px #ccc+ & + td:+ background : #eee url('../img/shadow24.png') repeat-y (-40px) 0+ padding-left : 2em++ th:+ background : #ddd+ text-align : left+ border-bottom : solid 1px #bbb++ #author:+ width : 200px++ #modified:+ width : 220px++ a:+ color : #b47+ text-decoration : none+ cursor : pointer+ &:hover:+ color : #305+ a:- margin-right : 0.6em+ margin-right : 0.8em margin-bottom : 0.4em padding : 0.4em @@ -44,6 +84,9 @@ cursor : pointer + span:+ font-size : 0.7em+ &:hover: color : $linkHover @@ -77,6 +120,8 @@ input: background : #fff url('../img/shadow02.png') repeat-x 0 -20px padding : 0.2em+ margin-right : 1em+ width : 18em border : none color : #555 font->@@ -90,6 +135,9 @@ color : #a47 font-style : normal + &.checkbox:+ color : red+ textarea: background : #bbb url('../img/shadow24.png') repeat-y (-30px) 0 width : 100%@@ -115,52 +163,27 @@ font-size : 0.8em font-weight : normal + br:+ margin-bottom : 1.2em++// ---- specific UI components ------------------------------------------------+ #history: background : #ccc- padding : 1em border-bottom : solid 2px #fff display : none - table:- width : 100%- padding : 0.5em- font-size : 0.7em- line-height : 1.3em-- td, th:- padding : 0.4em- padding-left : 2em- vertical-align : top- font-size : 1.1em-- .current td:- background : #fff-- td:- background : #eee- text-align : left- border-top : solid 1px #ccc- & + td:- background : #eee url('../img/shadow24.png') repeat-y (-40px) 0- padding-left : 2em-- th:- background : #ddd- text-align : left- border-bottom : solid 1px #bbb- #author: width : 200px #modified: width : 220px - a:- color : #b47- text-decoration : none- cursor : pointer- &:hover:- color : #305+ .status:+ display : block+ float : none+ text-align : right+ margin-top : 0 #info: font-size : 0.8em@@ -176,8 +199,6 @@ font-weight : normal #auth:- input:- width : 14em #user-ui: display : none #username:@@ -185,13 +206,11 @@ margin-right : 1em #signup:- input:- width : 14em #su-username: &, #su-password: margin-right : 1em - #editor #change:+ #editor #change, #editor #rename: margin-bottom : 1em width : 100% display : block@@ -205,6 +224,21 @@ margin-> left : 0.4em right : 0++ #search:+ #search-terms:+ width : 80%++ table:+ margin-top : 1.6em++ #resource:+ width : 280px++ #linenumber:+ width : 40px++// ---- additional UI elements ------------------------------------------------ #full-btn: margin->