UNPKG

http-server-editor

Version:

HTTP server that accepts user-triggered changes to static HTML & Markdown pages and dynamic .elm pages.

94 lines (79 loc) 384 kB
var params = process.argv.slice(2); function getParam(x, defaultValue) { var param = params.find(elem => elem.startsWith(x)); if(typeof param !== "undefined") { var returnValue = param.substring(x.length + 1); if(returnValue == "") return defaultValue; return returnValue; } else { return defaultValue; } } function existsParam(x) { var param = params.find(elem => elem == x); return typeof param !== "undefined"; } function isBoolParamTrue(x) { return existsParam(x) || getParam(x, "false") == true; } function getNonParam() { return params.find(elem => !elem.startsWith("-")) } const fs = require("fs"); const fspath = require('path'); const https = require('https'); //const http = require('http'); const url = require('url'); const hostname = getParam("hostname", 'localhost'); var port = parseInt(getParam("port", "3000")); // The default client id is suitable only for localhost:3000 var googleClientId = getParam("google-client-id", "844835838734-2iphm3ff20ephn906md1ru8vbkpu4mg8.apps.googleusercontent.com"); const getPort = require('get-port'); const serverFile = "./server.elm"; const htaccessFile = "./htaccess.elm"; var path = getParam("--path", ""); var question = getParam("--question", "true") == "true"; var autosave = getParam("--autosave", "false") == "true"; var fileToOpen = getNonParam(); if(fileToOpen) { if(fileToOpen.indexOf("/") == -1 && fileToOpen.indexOf("\\") == -1) { path = ""; } else { path = fileToOpen.replace(/[\/\\][^\/\\]*$/g, ""); } fileToOpen = fileToOpen.replace(/.*[\/\\](?=[^\/\\]*$)/g, ""); question = false; // When opening a file, by default we should not ask questions. autosave = false; // When opening a file, by default we want to let the user save it. } var timeBeforeExit = 2000; // Number of ms after receiving the closing signal (if file open) to kill the server. leo = {}; leo.data = function(name) { return function() { let args = {}; for(let i = 1; i <= arguments.length; i++) { args["_" + i] = arguments[i - 1]; } return {"$d_ctor": name, args: args}; } } leo.Just = leo.data("Just") leo.Nothing = leo.data("Nothing")() leo.Tuple2 = function(a, b) { return {"$t_ctor": "Tuple2", _1: a, _2: b} } var defaultOptions = { edit: getParam("--edit", "true") == "true", autosave: autosave, question: question, admin: isBoolParamTrue("--admin"), production: isBoolParamTrue("--production"), path: path, closeable: !(!(fileToOpen)), openbrowser: isBoolParamTrue("--openbrowser"), key: "localhost-key.pem", cert: "localhost.pem", hyde: getParam("--hyde", "true") == "true" }; async function start() { // Don't modify, this will be replaced by the content of 'server.elm' const defaultServerContent = "-- input: path The file to serve.\n-- input: vars: URL query vars.\n-- input: urlParams: The URL params plainly\n-- input: defaultOptions default options (options that vars can override for certain parts).\n-- If nodefs is set, will use it instead of nodejs.nodeFS\n-- If browserSide is set, will use a different kind of request. \n-- input: fileOperations The current set of delayed file disk operations.\n-- Note that Elm pages are given in context the path, the vars, and the file system (fs) to read other files\n-- output: The page, either (almost) uninstrumented or augmented with the toolbar and edit scripts.\n\n{--------------------------------------------------------\n Permission handling, file system, options processing\n---------------------------------------------------------}\nlistGetOrElse key listDict default = listDict.get key listDict |> Maybe.withDefault default\n\n{--}\nupdatecheckpoint name x = {\n apply x = x\n update {input, outputNew, diffs} =\n let _ = Debug.log \"\"\"Checkpoint @name\"\"\" () in \n Ok (InputsWithDiffs [(outputNew, Just diffs)])\n}.apply x\n\ndebugcheckpoint name x = let _ = Debug.log name () in x\n--}\n\nserverOwned what obj = freezeWhen (not permissionToEditServer) (\\od -> \"\"\"You tried to modify @what, which is part of the server. We prevented you from doing so.<br><br>\n\nIf you really intended to modify this, add ?superadmin=true to the URL and redo this operation. This is likely going to create or modify the existing <code>server.elm</code> at the location where you launched Editor.<br><br>\n\nFor debugging purposes, below is the new value that was pushed:\n<pre>@(Regex.replace \"<\" (always \"&lt;\") \"\"\"@od\"\"\")</pre>\nHere is the old value that was computed\n<pre>@(Regex.replace \"<\" (always \"&lt;\") \"\"\"@obj\"\"\")</pre>\n\"\"\") obj\n\n\npreludeEnv = __CurrentEnv__\n\nmbApplyPrefix = case listDict.get \"path\" defaultOptions of\n Just \"\" -> Nothing\n Nothing -> Nothing\n Just prefix -> Just (\\name -> if name == \"\" then prefix\n else if Regex.matchIn \"/$\" prefix then prefix + name\n else prefix + \"/\" + name)\n\ndirectReadFileSystem =\n listDict.get \"nodefs\" defaultOptions |> Maybe.withDefault nodejs.nodeFS\n\nfs = nodejs.delayedFS directReadFileSystem fileOperations\n\nhydefilecache = listDict.get \"hydefilecache\" defaultOptions\n\nfs = case mbApplyPrefix of\n Nothing -> fs\n Just applyPrefix -> { fs |\n read name = fs.read (applyPrefix name)\n listdir name = fs.listdir (applyPrefix name)\n listdircontent name = fs.listdircontent (applyPrefix name)\n isdir name = fs.isdir (applyPrefix name)\n isfile name = fs.isfile (applyPrefix name)\n }\n\nboolVar name resDefault =\n listDict.get name vars |>\n Maybe.map (\\original ->\n Update.bijection\n (case of \"true\" -> True; \"\" -> True; _ -> False)\n (case of True -> if original == \"true\" || original == \"\" then original else \"true\"; _ -> \"false\") original) |>\n Maybe.withDefaultReplace (\n listDict.get name defaultOptions |> Maybe.withDefault resDefault |> freeze)\n\nbrowserSide = listDict.get \"browserSide\" defaultOptions == Just True\n\nvaradmin = boolVar \"admin\" False\nvarraw = boolVar \"raw\" False\nvaredit = boolVar \"edit\" False || varraw\nvarls = boolVar \"ls\" False\nvarclearhydecache = boolVar \"clearhydecache\" False\nvarhydeNotDisabled = boolVar \"hyde\" True\ndefaultVarEdit = listDict.get \"edit\" defaultOptions |> Maybe.withDefault False\nvarproduction = listDict.get \"production\" defaultOptions |> Maybe.withDefault (freeze False)\niscloseable = listDict.get \"closeable\" defaultOptions |> Maybe.withDefault (freeze False)\nvarFast = boolVar \"fast\" False\n\nuserpermissions = {pageowner= True, admin= varadmin}\npermissionToCreate = userpermissions.admin\npermissionToEditServer = boolVar \"superadmin\" False -- should be possibly get from user authentication\n-- List.contains (\"sub\", \"102014571179481340426\") user -- That's my Google user ID.\n\ncanEditPage = userpermissions.pageowner && varedit && not varls\n\nfreezeWhen = Update.freezeWhen\n\ncanEvaluate = listDict.get \"evaluate\" vars |> Maybe.withDefaultReplace (serverOwned \"default value of evaluate\" \"true\")\n\n{--------------------------------------------------------\n Rewrite path to either a folder or a default file under\n---------------------------------------------------------}\n\npath: String\npath =\n if fs.isdir path then\n if not varls then\n List.mapFirstSuccess (\\test ->\n if fs.isfile <| path + test then Just (path + test) else Nothing)\n [\"index.elm\" , \"/index.elm\", \"index.html\", \"/index.html\", \"README.md\" , \"/README.md\" ]\n |> Maybe.withDefault path\n else path\n else path\n\n{---------------------------------------------------------------------------\n Retrieves the string content of the path. For folders, creates a custom page\n----------------------------------------------------------------------------}\n\nmbDotEditorFile = \n let prefix = Regex.extract \"^(.*/)[^/]*$\" path |> Maybe.map (\\[prefix] -> prefix) |> Maybe.withDefault \"\" in\n let dotEditor = prefix + \".editor\" in\n fs.read dotEditor\n\napplyDotEditor source = \n let prefix = Regex.extract \"^(.*/)[^/]*$\" path |> Maybe.map (\\[prefix] -> prefix) |> Maybe.withDefault \"\" in\n let dotEditor = prefix + \".editor\" in\n case mbDotEditorFile of\n Nothing -> source\n Just modifier ->\n case __evaluate__ ((\"vars\", vars)::(\"path\", path)::(\"fs\", fs)::(\"content\", source)::preludeEnv) modifier of\n Err msg -> let _ = Debug.log (\"Error while executing \" + dotEditor + \" : \" + msg) () in\n source\n Ok newSource -> newSource\n\nisTextFile path =\n Regex.matchIn \"\"\"\\.(?:txt|css|js|sass|scss)$\"\"\" path\n\n---------------------\n-- Hyde file support.\n-- Everything unrolled on the main let definitions to benefit from caching\n---------------------\n\n(withoutPipeline, hydefilepath, hydefileSource) = case hydefilecache of\n Just {file=hydefile} ->\n case if varclearhydecache then Nothing else hydefilecache of\n Just {cacheContent} ->\n case evaluate cacheContent of\n {inputFiles, outputFiles} ->\n (not (List.isEmpty outputFiles) &&\n List.find (\\e -> e == path || e == \"/\" + path) inputFiles == Nothing &&\n List.find (\\e -> e == path || e == \"/\" + path) outputFiles == Nothing,\n hydefile, fs.read hydefile)\n _ -> (False, hydefile, fs.read hydefile)\n _ -> (False, hydefile, fs.read hydefile) -- Need to recompute the cache anyway\n _ -> (True, \"\", Nothing)\n\n(folderView, mbSourcecontentAny1, hydeNotNeeded): (Boolean, Maybe String, Boolean)\n(folderView, mbSourcecontentAny1, hydeNotNeeded) =\n if path == \"server.elm\" then\n (False, Just \"\"\"<html><head></head><body class=\"editor-error\">The Elm server cannot display itself. This is a placeholder</body></html>\"\"\", True)\n else if fs.isdir path then\n (True, Just \"\", True)\n else if fs.isfile path && Regex.matchIn \"\"\"\\.(png|jpg|ico|gif|jpeg)$\"\"\" path then -- Normally not called because server.js takes care of these cases.\n (False, Just \"\"\"<html><head><meta name=\"viewport\" content=\"width=device-width, minimum-scale=0.1\"><title>@path</title></head><body style=\"margin: 0px; background: #0e0e0e;\"><img style=\"-webkit-user-select: none;margin: auto;\" src=\"@path\"></body></html>\"\"\", True)\n else if hydefilecache == Nothing || withoutPipeline || not varhydeNotDisabled then\n (False, fs.read path, True)\n else\n (False, Nothing, False)\n\nhyde_sourceRaw = if hydeNotNeeded then \"\" else\n hydefileSource |> Maybe.withDefaultLazy (\\_ -> \"\"\"all = [Error \"hyde file '@hydefilepath' not found?!\"]\"\"\")\n\nhyde_source = if hydeNotNeeded then \"\" else\n hyde_sourceRaw + Update.freeze \"\\n\\nlet t = \" + (listDict.get \"task\" vars |> Maybe.withDefault \"all\") + \"\\n t = if typeof t == 'function' then t () else t\\n t = if typeof t == 'list' then t else [t]\\nin t\"\n\nhyde_fileDirectory = if hydeNotNeeded then \"\" else\n Regex.replace \"/[^/]*$\" \"\" hydefilepath\n\nhyde_inDirectory name =\n if hyde_fileDirectory == \"\" then name else\n hyde_fileDirectory + \"/\" + name\n\nhyde_fs = \n if hydeNotNeeded then {} else\n let hyde_fsReadRecord = \n { directReadFileSystem |\n read name =\n let name = hyde_inDirectory name in\n let _ = recordFileRead name in\n fs.read name,\n listdir name =\n let name = hyde_inDirectory name in\n let _ = recordFolderList name in\n fs.listdir name\n } in\n nodejs.delayedFS hyde_fsReadRecord <|\n Update.lens {\n apply = identity\n update {outputNew, diffs} = -- input and outputOld were empty, diffs is valid\n -- We just need to change the paths\n outputNew |>\n List.map (case of\n (name, Rename newName) -> (hyde_inDirectory name, Rename (hyde_inDirectory newName))\n (name, x) -> (hyde_inDirectory name, x))|>\n flip (,) (Just diffs) |>\n List.singleton |> InputsWithDiffs |> Ok\n } fileOperations\n\n-- A Hyde plug-is a function that takes two arguments\n-- options (as a list, object or tuple)\n-- a list of Write\n-- Returns a list of Write\nplugin name =\n fs.read (hyde_inDirectory (\"hydefile-plugin-\" + name + \".leo\")) |>\n Maybe.andThen (\\plugin_content ->\n case __evaluate__ ((\"fs\", hyde_fs)::(\"plugin\", plugin)::preludeEnv) plugin_content of\n Ok x -> Just x\n Err msg -> \n let _ = Debug.log (\"loading of plugin \" + name + \" failed:\" + msg) () in\n Nothing\n ) |> Maybe.withDefaultLazy (\\_ ->\n let _ = Debug.log (\"plugin \" + name + \" not found\") () in\n \\options -> identity)\n\nhyde_resEvaluatedHydeFile =\n if hydeNotNeeded then {} else\n __evaluateWithCache__ ((\"fs\", hyde_fs)::(\"plugin\", plugin)::preludeEnv) hyde_source\n\n(hyde_generatedFilesDict, hyde_errors) =\n if hydeNotNeeded then (False, False) else\n case hyde_resEvaluatedHydeFile of\n Err msg -> ([], msg)\n Ok (writtenContent, _) ->\n let (written, errors) = List.partition (case of Write -> True; _ -> False) writtenContent in\n let tuplesToWrite =\n List.map (case of Write name content -> (hyde_inDirectory name, content)) written\n joinedErrors = \n List.map (case of Error msg -> msg) errors |> String.join \"\\n\"\n in\n (tuplesToWrite, joinedErrors)\n\nhyde_record_output_files =\n if hydeNotNeeded then False else\n let hyde_dummy = recordOutputFiles hyde_generatedFilesDict in -- Writes on disk \n False\n\nhyde_dummy = if hydeNotNeeded then (False, False) else\n let x = hyde_generatedFilesDict in -- to make sure dependency is executed\n if hyde_errors == \"\" then\n cacheResult () -- Caches the names of written files\n else \n ()\n\nmbSourcecontentAny =\n if hydeNotNeeded then mbSourcecontentAny1 else\n case listDict.get (\"/\" + path) hyde_generatedFilesDict of\n Nothing ->\n case listDict.get path hyde_generatedFilesDict of\n Nothing ->\n if hyde_errors == \"\" then\n let _ = Debug.log \"\"\"Unable to read (/)@path from output of hydefile\"\"\" () in\n fs.read path\n else\n Just <|\n serverOwned \"error recovery of hyde build tool\" <|\n \"\"\"<html><head></head><body class=\"editor-error\"><h1>Error while resolving the generated version of @path</h1><pre>@hyde_errors</pre></body></html>\"\"\"\n x -> x\n x -> x\n\n---------------------------------\n-- Hyde file support ends here --\n---------------------------------\n\nsourcecontentAny = Maybe.withDefaultReplace (\n serverOwned \"404 page\" (if isTextFile path then\n if permissionToCreate then freeze \"\"\"@path does not exist yet. Modify this page to create it!\"\"\" else \"\"\"Error 404, @path does not exist or you don't have admin rights to modify it (?admin=true)\"\"\"\n else \"\"\"<html><head></head><body class=\"editor-error\">@(\n if permissionToCreate then freeze \"\"\"<span>@path does not exist yet. Modify this page to create it!</span>\"\"\" else \"\"\"<span>Error 404, @path does not exist or you don't have admin rights to modify it (?admin=true)</span>\"\"\"\n )</body></html>\"\"\")\n ) mbSourcecontentAny\n\nsourcecontent = String.newlines.toUnix sourcecontentAny\n\n-- Conversion of php script to elm script\nphpToElmFinal path string =\n let includingFolder = Regex.replace \"\"\"(/)[^/]*$\"\"\" (\\{submatches=[slash]} -> slash) path in\n let phpToElm string =\n let echoRaw content = \"\\nob = ob + \" + content in\n let wrapStr content = freeze String.q3 + Regex.replace \"@\" (\\{match=m} -> m + m) content + freeze String.q3 in\n let echo content = echoRaw (wrapStr content) in\n let phpStringToElmString =\n (Regex.replace \"\"\"(\\\")([^\\\"]*)(\\\")\"\"\" <| \\m ->\n nth m.group 1 +\n (nth m.group 2\n |> Regex.replace \"\"\"\\$[0-9a-zA-Z_]*\"\"\" (\\n ->\n freeze \"\\\" + \" + nth n.group 0 + freeze \" + \\\"\")) +\n nth m.group 3) >>\n (Regex.replace \"\"\"\\$_GET\\[([^\\]]*)\\]\"\"\" <| \\m ->\n freeze \"listDict.get \"+ nth m.group 1 + freeze \" $_GET |> Maybe.withDefaultReplace ''\"\n ) >>\n (Regex.replace \"\"\"\\$_SERVER\\[([^\\]]*)\\]\"\"\" <| \\m ->\n freeze \"listDict.get \"+ nth m.group 1 + freeze \" $_SERVER |> Maybe.withDefaultReplace ''\"\n )\n in\n if not (Regex.matchIn \"<?php\" string) then\n echo string\n else\n Regex.replace \"\"\"^((?:(?!<\\?php)[\\s\\S])+?)(?=(<\\?php))|(\\?>)([\\s\\S]*?)(?=<\\?php)|(\\?>)([\\s\\S]*?)$|(^)(<\\?php)([\\s\\S]*?)(?=\\?>)|(<\\?php)\\s*if\\s*\\(([\\s\\S]*?)\\s*\\)\\s*\\{\\s*\\?>((?:(?!<\\?php)[\\s\\S])+?)<\\?php\\s*\\}\\s*(?=\\?>)|(<\\?php)([\\s\\S]*?)(?=\\?>)\"\"\" (\n \\{submatches=[content1, isRaw1, isRaw2, content2, isRaw3, content3, beginning1, isPhp1, code1, isPhpIf, condIf, codeIf, isPhp2, code2]} ->\n if isPhp1 /= \"\" || isPhp2 /= \"\" || isPhpIf /= \"\" then\n let prefix = if isPhp1 /= \"\" then echo beginning1 else freeze \"\" in\n prefix +\n if isPhpIf /= \"\" then\n echoRaw <| \"(if \"+condIf+\" then \" + wrapStr codeIf + \" else \\\"\\\")\"\n else\n let code = if isPhp1 /= \"\" then code1 else code2 in\n case Regex.extract \"\"\"^\\s*include\\(\"([^\"]*)\"\\)\"\"\" code of\n Just [included] ->\n phpToElm (fs.read (includingFolder + included) |> Maybe.withDefaultReplace (\"\\n[code to read \" + included + \" in \" + includingFolder +\"]\"))\n _ ->\n case Regex.extract \"\"\"^\\s*switch\\s*\\(([^\\)]*)\\)\\s*\\{((?:\\s*(?:case\\s*[^:]*?\\s*|default):((?:\\s*\\$[\\w_]+\\s*=\\s*(?:(?!;\\r?\\n)[\\s\\S])*;)*)(?:\\s*break\\s*;)?)*)\\s*\\}\\s*\"\"\" code of\n Just [input, assignments, lastAssignment] ->\n let vars = \"(\" + (Regex.find \"\"\"(\\$[\\w_]+)\\s*=\"\"\" lastAssignment |> List.map (\\[_, name] -> name) |> String.join \", \") + \")\" in\n let results = assignments |> Regex.find \"\"\"\\s*(case\\s*([^:]*?)\\s*|default):((?:\\s*\\$[\\w_]+\\s*=\\s*(?:(?!;\\r?\\n)[\\s\\S])*;)*)(?:\\s*break\\s*;)?\"\"\" |>\n List.map (\\[whole, caseOrDefault, pattern, values] ->\n let tuple =\n Regex.find \"\"\"\\s*\\$[\\w_]+\\s*=\\s*((?:(?!;\\r?\\n)[\\s\\S])*?)\\s*;\"\"\" values |>\n List.map (\\[whole2, value2] -> phpStringToElmString value2) |> String.join \", \"\n in\n let finalPattern = if caseOrDefault == \"default\" then \"_\" else pattern in\n \"\\n \" + finalPattern + \" -> (\" + tuple + \")\"\n ) |> String.join \"\"\n in\n \"\\n\" + vars + \" = case \" + phpStringToElmString input + \" of\" + results\n _ ->\n case Regex.extract \"\"\"\\s*(?:echo|print)\\s+([^;]+?);\\s*\"\"\" code of\n Just [content] -> echoRaw content\n _ ->\n case Regex.extract \"\"\"\\s*([\\$\\w_]+\\s*=\\s*)([\\s\\S]+?)\\s*;(?=\\r?\\n)\"\"\" code of\n Just [varNameEqual,toAssign] -> \"\\n\" + varNameEqual + phpStringToElmString toAssign\n res ->\n \"\\n[convert\" + code + \"]\\n\" + toString res\n else\n let content = if isRaw1 /= \"\" then\n content1\n else if isRaw2 /= \"\" then\n content2\n else -- if isRaw3 /= \"\" then\n content3\n in\n echo content\n ) string\n in\n flip (+) \"\\nob\" <|\n (+) \"date _ = '2019'\\nob = freeze ''\" <| phpToElm string\n\n{---------------------------------------------------------------------------\n Evaluates the page according to the path extension.\n - Wraps html pages to parse them as raw html\n - Interprets markdown pages and evaluate them as raw html with CSS\n - Directly evaluate sources from elm/leo pages or folders\n----------------------------------------------------------------------------}\nevaluatedPage: Result String (List HtmlNode)\nevaluatedPage = \n if canEvaluate /= \"true\" then\n Ok [<html><head></head><body class=\"editor-error\">URL parameter evaluate=@(canEvaluate) requested the page not to be evaluated</body></html>]\n else if isTextFile path || varraw then\n Ok [<html style=\"height:100%;\">\n <head>\n <title>@path</title>\n <style type=\"text/css\" media=\"screen\">\n #aceeditor { \n height: 100%;\n width: 100%;\n border: 1px solid #DDD;\n border-radius: 4px;\n border-bottom-right-radius: 0px;\n margin-top: 5px;\n }\n </style>\n <script>\n function loadAceEditor() {\n console.log(\"executing script\");\n var aceeditor = ace.edit(\"aceeditor\");\n var mode = editor.config.path.match(/\\.js$/) ? \"ace/mode/javascript\" :\n editor.config.path.match(/\\.html?$/) ? \"ace/mode/html\" :\n editor.config.path.match(/\\.css$/) ? \"ace/mode/css\" :\n editor.config.path.match(/\\.json$/) ? \"ace/mode/json\" :\n editor.config.path.match(/\\.leo$/) ? \"ace/mode/elm\" :\n editor.config.path.match(/\\.elm$/) ? \"ace/mode/elm\" :\n editor.config.path.match(/\\.php$/) ? \"ace/mode/php\" :\n \"ace/mode/plain_text\";\n aceeditor.session.setMode({path: mode, v: Date.now()});\n aceeditor.setOptions({\n fontSize: \"20pt\"\n });\n aceeditor.setValue(document.getElementById(\"aceeditor\").getAttribute(\"initdata\"));\n aceeditor.session.on('change', function(e) {\n document.getElementById(\"aceeditor\").setAttribute(\"initdata\", aceeditor.getValue());\n });\n var callbackSelection = function() {\n var anchor = aceeditor.selection.getSelectionAnchor();\n var lead = aceeditor.selection.getSelectionLead();\n var div = document.querySelector(\"#aceeditor\");\n div.setAttribute(\"ghost-anchor-row\", anchor.row)\n div.setAttribute(\"ghost-anchor-column\", anchor.column)\n div.setAttribute(\"ghost-lead-row\", lead.row)\n div.setAttribute(\"ghost-lead-column\", lead.column)\n }\n aceeditor.selection.on(\"changeSelection\", callbackSelection);\n aceeditor.selection.on(\"changeCursor\", callbackSelection);\n var div = document.querySelector(\"#aceeditor\");\n aceeditor.selection.moveTo(div.getAttribute(\"ghost-anchor-row\") || 0, div.getAttribute(\"ghost-anchor-column\") || 0)\n aceeditor.focus();\n }\n </script>\n </head>\n <body style=\"height:100%\">\n <div id=\"aceeditor\" list-ghost-attributes=\"class draggable style\" children-are-ghosts=\"true\"\n save-ghost-attributes=\"style ghost-anchor-column ghost-anchor-row ghost-lead-column ghost-lead-row\" initdata=@sourcecontent></div>\n <script>\n editor.ghostNodes.push(node =>\n node.tagName === \"SCRIPT\" && node.getAttribute(\"src\") && node.getAttribute(\"src\").match(/mode-(.*)\\.js|libs\\/ace\\/.*\\/ext-searchbox.js/)\n );\n \n var script = document.createElement('script');\n script.src = 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.6/ace.js';\n script.async = false;\n script.setAttribute(\"isghost\", \"true\");\n ace = undefined;\n document.head.appendChild(script);\n onAceLoaded = (delay) => () => {\n if(typeof ace != \"undefined\") {\n console.log(\"ace loaded.\")\n loadAceEditor();\n } else {\n console.log(\"ace not loaded. Retrying in \" + (delay * 2) + \"ms\");\n setTimeout(onAceLoaded(delay * 2), 100);\n }\n }\n onAceLoaded(1)();\n </script>\n </body>\n </html>]\n else \n let isPhp = Regex.matchIn \"\"\"\\.php$\"\"\" path in\n let isHtml = Regex.matchIn \"\"\"\\.html?$\"\"\" path in\n if isHtml || isPhp then\n let sourcecontent = if isHtml then applyDotEditor sourcecontent else\n let elmSourceContent = phpToElmFinal path sourcecontent in\n __evaluate__ ((\"$_GET\", vars)::(\"$_SERVER\", [(\"SCRIPT_NAME\", \"/\" + path)])::(\"path\", path)::(\"fs\", fs)::preludeEnv) elmSourceContent |>\n case of\n Err msg -> serverOwned \"error message\" \"<html><head></head><body class=\"editor-error\"><pre>Error elm-reinterpreted php: \" + Regex.replace \"<\" \"&lt;\" msg + \"</pre>Original computed source <pre>\" +\n Regex.replace \"<\" \"&lt;\" elmSourceContent +\n \"</pre></body></html>\"\n Ok sourcecontent -> applyDotEditor sourcecontent\n in\n let interpretableData = serverOwned \"begin raw tag\" \"<raw>\" + sourcecontent + serverOwned \"end raw tag\" \"</raw>\" in\n __evaluate__ preludeEnv interpretableData |>\n Result.andThen (case of\n [\"raw\", _, nodes] -> Ok nodes\n result -> Err \"\"\"Html interpretation error: The interpretation of raw html did not work but produced @result\"\"\"\n )\n else if Regex.matchIn \"\"\"\\.md$\"\"\" path then\n let markdownized = String.markdown sourcecontent in\n case Html.parseViaEval markdownized of\n x -> \n let markdownstyle = fs.read \"markdown.css\" |> Maybe.withDefaultReplace defaultMarkdowncss in\n Ok [<html><head></head><body><style title=\"If you modify me, I'll create a custom markdwon.css that will override the default CSS for markdown rendering\">@markdownstyle</style><div class=\"wrapper\">@x</div></body></html>]\n else if Regex.matchIn \"\"\"\\.(elm|leo)$\"\"\" path then\n let res = __evaluate__ ((\"vars\", vars)::(\"path\", path)::(\"fs\", fs)::preludeEnv) sourcecontent in\n case res of\n [\"html\", _, _] -> [res]\n _ -> res\n else if folderView then\n Ok [<html><head>\n <script>\n var ispressed = false;\n var whichOne = \"\";\n //declare bool variable to be false\n document.onkeydown = function(e) {\n if (e.ctrlKey){\n ispressed = true;\n }\n };\n document.onkeyup = function(e) {\n if (e.keyCode == 17){ //releasing ctrl key. doesn't set e.ctrlKey properly or would use that.\n ispressed = false;\n }\n }\n var handleFileSelect = e => {\n e.preventDefault();\n }\n document.addEventListener('drop', handleFileSelect, false);\n </script>\n <style>\n #menu_bar {\n overflow: hidden;\n background-color: #ffffff;\n opacity:1;\n }\n\n #menu_bar a {\n float: left;\n display: block;\n color: #f2f2f2;\n text-align: center;\n padding: 14px 16px;\n text-decoration: none;\n font-size: 17px;\n }\n\n #menu_bar a:hover {\n background-color: #ddd;\n color: black;\n }\n\n #menu_bar a.active {\n background-color: #4CAF50;\n color: white;\n }\n .dropdown {\n float: left;\n overflow: hidden;\n }\n\n .dropdown .dropbtn {\n font-size: 16px; \n border: none;\n outline: none;\n color: white;\n padding: 14px 16px;\n background-color: inherit;\n font-family: inherit;\n margin: 0;\n }\n .dropdown .dropbtn {\n font-size: 16px; \n border: none;\n outline: none;\n color: white;\n padding: 14px 16px;\n background-color: inherit;\n font-family: inherit;\n margin: 0;\n }\n\n .menu_bar a:hover, .dropdown:hover .dropbtn {\n background-color: red;\n }\n\n .dropdown-content {\n display: none;\n position: absolute;\n background-color: #f9f9f9;\n min-width: 160px;\n box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);\n z-index: 1;\n }\n\n .dropdown-content a {\n float: none;\n color: black;\n padding: 12px 16px;\n text-decoration: none;\n display: block;\n text-align: left;\n }\n\n .dropdown-content a:hover {\n background-color: #ddd;\n }\n\n .dropdown:hover .dropdown-content {\n display: block;\n }\n .content {\n padding: 16px;\n }\n\n .sticky {\n position: fixed;\n top: 0;\n width: 100%;\n }\n\n .sticky + .content {\n padding-top: 60px;\n }\n #fileListing div.file-item {\n display: block;\n }\n #fileListing div.file-item input {\n display: none;\n }\n #fileListing div.file-item {\n display: table-row;\n }\n #fileListing div.file-item label {\n display: table-cell;\n vertical-align: middle;\n padding: 0.3em;\n }\n #fileListing div.file-item label:hover {\n background: rgb(229,243,255);\n }\n #fileListing div.file-item input:checked + label {\n color: white;\n outline: 1px solid rgb(153,209,255);\n background: rgb(204,232,255);\n }\n #fileListing div.file-item label a {\n text-decoration: none;\n color: black;\n padding: 2px;\n }\n #fileListing div.file-item label a:hover {\n text-decoration: underline;\n color: blue;\n }\n #fileListing div.file-item label svg {\n vertical-align: middle;\n transform: scale(0.5);\n }\n #fileListing div.file-item label svg.file-extension-icon {\n opacity: 0.5;\n }\n #fileListing div.file-item label svg.file-extension-icon > path {\n stroke:black;\n stroke-width:2px;\n stroke-linecap:butt;\n fill:none;\n -linejoin:miter;\n stroke-opacity:1;\n }\n #fileListing div.file-item label svg.file-extension-icon > text {\n font-size: 2em;\n }\n\n </style>\n <div id=\"menu_bar\">\n <button id=\"renamefs\" onClick=\"renameFs()\">Rename File(s)</button>\n <button id=\"duplicatefs\" onClick=\"duplicateFs()\">Make a Copy</button>\n <button id=\"movefs\" onClick=\"moveFs()\">Move File(s)</button>\n <button id=\"createFolder\" onClick=\"createFolder()\">Create a Folder</button>\n <button id=\"deletefs\" onClick=\"deleteFs()\">Delete File(s)</button>\n <div id=\"forprog\"></div>\n </div>\n </head><body><h1><label value=path>@path</label></h1>\n <form id=\"fileListing\"></form>\n <script>\n el = editor.el;\n var fullListDir = (path) => JSON.parse(editor._internals.doReadServer(\"fullListDir\", path));\n var thisListDir = fullListDir(editor.config.path);\n var folders = thisListDir.filter((i) => i[1] == true);\n var getSelectedFiles = () => Array.from(document.querySelectorAll(\"input.filesBtn\")).filter((btn) => btn.checked);\n var warnSelectFile = reason => window.alert (reason + \", please select some and click this button again\");\n var warnDeselectFiles = reason => window.alert (reason + \", please deselect all files and folders and click this button again\");\n var isDupInFolder = (folder, name) => folder.filter((i) => i[0] == name).length != 0;\n var isDuplicateHere = (name) => isDupInFolder(thisListDir, name);\n var isFolder = (name) => folders.filter((i) => i[0] == name).length != 0;\n\n window.onscroll = function() {stickyFun()};\n var menu_bar = document.getElementById(\"menu_bar\");\n var sticky = menu_bar.offsetTop;\n\n function stickyFun() {\n if (window.pageYOffset >= sticky) {\n menu_bar.classList.add(\"sticky\")\n } else {\n menu_bar.classList.remove(\"sticky\");\n }\n }\n function getOneFile(reason) {\n var selected = getSelectedFiles();\n if (selected.length == 0) {\n warnSelectFile(reason);\n return 0;\n } else if (selected.length != 1) {\n window.alert (\"Please select only one file to rename\");\n return 0;\n }\n return selected[0];\n }\n function renameFs() {\n console.log (\"in rename fs\");\n var sel = getOneFile(\"To rename files or folders\");\n if (! sel) return;\n if (sel.id == \"..\") {\n window.alert(\"Can't change the up dir\");\n return;\n }\n var newname = window.prompt(\"Set new name for file: \", sel.id);\n if (newname == null) return;\n if (newname == \"\") {\n window.alert(\"Please specify a new name for the file.\");\n return;\n }\n if (isDuplicateHere(newname)) {\n const doit = window.confirm(\"Are you sure you want to overwrite an existing file with the name \" + newname + \"?\");\n if (!doit) return;\n }\n var x = editor._internals.doWriteServer(\"rename\", editor.config.path + sel.id, editor.config.path + newname);\n console.log (\"renamed\", sel.id, newname);\n goodReload();\n }\n function deleteFs() {\n var selected = getSelectedFiles();\n if (selected.length == 0) {\n warnSelectFile(\"To delete a file or a folder\"); \n return;\n }\n if (selected.filter((i) => i.id == \"..\").length != 0) {\n window.alert(\"Can't delete the parent dir\");\n return;\n }\n var warningMsg = \"Are you sure you want to delete the following file(s)?\"\n for (i = 0; i < selected.length; i++) {\n warningMsg = warningMsg + \"\\n\" + selected[i].id;\n }\n var conf = window.confirm(warningMsg);\n if (conf) {\n for (i = 0; i < selected.length; i++) {\n var isfolder = folders.filter((j) => j[0] == selected[i].id); //optomizable\n console.log (isfolder);\n if (isfolder.length != 0) {\n editor._internals.doWriteServer(\"rmdir\", editor.config.path + selected[i].id); //does this work on non-empty stuff? idts....\n continue;\n }\n editor._internals.doWriteServer(\"unlink\", editor.config.path + selected[i].id);\n }\n goodReload();\n return;\n }\n }\n function duplicateFs() {\n var sel = getOneFile(\"To duplicate files or folders\");\n if (! sel) return;\n if (sel.id == \"..\") {\n window.alert(\"Can't change the up dir\");\n return;\n }\n var lastdot = sel.id.lastIndexOf(\".\");\n var nn;\n if (isFolder(sel.id)) {\n nn = sel.id + \"_(Copy)\";\n } else {\n nn = sel.id.substring(0, lastdot) + \"_(Copy)\" + sel.id.substring(lastdot);\n }\n var newname = window.prompt(\"Name for duplicate: \", nn);\n var contents = editor._internals.doReadServer(\"read\", editor.config.path + sel.id);\n if (contents[0] != \"1\") {\n window.alert (\"Couldn't read the file for some reason. aborting.\");\n console.error (\"couldn't read the file for some reason. aborting.\");\n return;\n }\n contents = contents.substring(1, contents.length);\n var resp = editor._internals.doWriteServer(\"create\", editor.config.path + newname, contents);\n goodReload();\n }\n function createFolder() {\n var btns = getSelectedFiles();\n if (btns.length != 0) {\n warnDeselectFiles(\"To create a folder\");\n return;\n }\n var newname = window.prompt(\"Name for new folder: \", \"\");\n console.log (newname);\n if (newname == null) return;\n if (newname == \"\") {\n window.alert(\"Please set a name for the new folder!\");\n return;\n }\n var dups = isDuplicateHere(newname);\n if (dups) {\n const conf = window.confirm (\"Are you sure you want to overwrite a folder with the name \" + newname + \" with an empty file? This would delete the folder.\");\n if (!conf) return;\n }\n editor._internals.doWriteServer(\"mkdir\", newname, \"\");\n goodReload();\n }\n function moveFs() {\n var btn = getOneFile(\"To move files or folders\");\n if (!btn) return;\n if (btn.id == \"..\") {\n window.alert(\"Can't change the up dir\");\n return;\n }\n var newpath = window.prompt(\"New path to file (relative to root of server):\", \"\");\n if (newpath == null) return;\n if (newpath[newpath.length -1] != \"/\") {\n newpath = newpath + \"/\";\n }\n try {\n var nldir = fullListDir(newpath);\n if (isDupInFolder(nldir, btn.id)) {\n const conf = window.confirm(\"Are you sure you want to overwrite an existing file?\");\n if (!conf) return;\n }\n } catch (e) {\n window.alert (\"The path specified does not exist. Move cancelled.\");\n return;\n }\n console.log (\"move approved\");\n var oldloc = (editor.config.path + btn.id);\n var newloc = newpath == \"/\" ? btn.id : (newpath + btn.id);\n console.log (\"renamimg\\n%s\\n%s\", (editor.config.path + btn.id), (newpath + btn.id));\n editor._internals.doWriteServer(\"rename\", oldloc, newloc); \n console.log (\"rename successful\");\n goodReload();\n }\n\n function radPressed(){\n var btns = document.querySelectorAll(\"input.filesBtn\");\n if (!ispressed){\n for(var i = 0; i < btns.length; i++){\n if (btns[i].value == whichOne) continue;\n btns[i].checked = false;\n }\n }\n }\n \n // Returns a progress bar or reuses the existing one.\n function initializeProgress() {\n var progressBar = document.getElementById(\"progress-bar\");\n if (!progressBar) {\n progressBar = el(\"progress\", {id:\"progress-bar\", max:\"100\", value:\"0\", visible:false}, [], {isghost: true});\n }\n progressBar.value = 0;\n progressBar.visible = true;\n return progressBar;\n }\n\n var handleFiles = (files) => {\n var pgbr = document.getElementById(\"forprog\");\n var progbar = initializeProgress();\n pgbr.append(progbar);\n var uploadProgress = {};\n var didUp = false;\n ([...files]).forEach((fl) => {\n var fileName = editor.config.path + fl.name;\n uploadProgress[fileName] = 0;\n });\n var callbackUpload = function (fileName, file, percent) {\n uploadProgress[fileName] = typeof percent == \"number\" ? percent : 100;\n let total = 0;\n let count = 0;\n for(var i in uploadProgress) {\n total += uploadProgress[i]\n count++;\n }\n progbar.value = total / count;\n if(total == 100) {\n progbar.visible = false;\n if (didUp) {\n goodReload();\n pgbr.innerHTML = \"\";\n }\n }\n }\n ([...files]).forEach((fl) => {\n var fileName = editor.config.path + fl.name;\n editor.uploadFile(fileName, fl,\n callbackUpload,\n (err) => {\n pgbr.innerHTML = \"\";\n console.err(err);\n },\n callbackUpload);\n didUp = true;\n });\n }\n function preventDefaults (e) {\n e.preventDefault()\n e.stopPropagation()\n }\n function handleDrop(e) {\n preventDefaults(e);\n let dt = e.dataTransfer;\n let files = dt.files;\n handleFiles(files);\n }\n function loadFileList() {\n let form = document.getElementById(\"fileListing\");\n form.innerHTML = \"\";\n let files = thisListDir;\n function getRecordForCheckbox(file) {\n var rec = {type:\"checkbox\",\n id:file,\n class:\"filesBtn\",\n name:\"filesBtn\",\n value:file,\n onClick:\"whichOne=value\",\n onChange:\"radPressed()\"};\n return rec;\n }\n var dirIcon = () => {\n var d = el(\"div\", {}, [], {innerHTML: \n `<svg class=\"file-extension-icon\" width=\"60\" height=\"30\">\n <path d=\"M 8,3 5,6 5,26 10,10 32,10 32,6 18,6 15,3 8,3 Z M 5,26 10,10 37,10 32,26 Z\" />`});\n return d.childNodes[0];\n }\n var extensionIcon = name => {\n let extension = name.replace(/^(?:(?!\\.(?=[^\\.]*$)).)*\\.?/, \"\");\n if(\".\" + extension == name || extension === \"\") extension = \"-\";\n var d = el(\"div\", {}, [], {innerHTML: \n `<svg class=\"file-extension-icon\" width=\"60\" height=\"30\">\n <text x=\"0\" y=\"25\">${extension}\n `});\n return d.childNodes[0];\n }\n\n var fileItemDisplay = function(name, isDir) {\n let newURL = name == \"..\" ?\n editor.config.path.replace(/(\\/|^)[^\\/]+\\/?$/, \"\")\n : editor.config.path + \"/\" + name;\n var link = typeof isDir == \"boolean\" ? (isDir ? newURL + \"/?ls\" : newURL + \"?edit\") : name;\n if(link.length > 0 && link[0] != \"/\") link = \"/\" + link;\n return el(\"div\", {class:\"file-item\"}, [\n el(\"input\", getRecordForCheckbox(name), \"\"),\n el(\"label\", {for:name, value:name}, [ \n isDir ? dirIcon() : extensionIcon(name),\n el(\"a\", {href:link}, name, {onclick: function(event) {\n event.preventDefault();\n if(isDir) {\n window.history.pushState({localURL: location.href}, name, link);\n editor.config.path = newURL;\n goodReload();\n } else {\n editor._internals.doReloadPage(link);\n }\n }})])]);\n }\n /*var otherItemDisplay = function(link, name) {\n return el(\"div\", {class:\"file-item\"}, [\n el(\"input\", getRecordForCheckbox(name), \"\"),\n el(\"label\", {for:name, value:name}, [ \n extensionIcon(name),\n el(\"a\", {href:link}, name)\n ])\n ]);\n }*/\n //el(tag, attributes, children, properties)\n if (editor.config.path != \"\") {\n var link = \"../\" + \"?ls\";\n form.append(fileItemDisplay(\"..\", true));\n }\n // directories before files, sorted case-insensitive\n files.sort(([name1, isDir1], [name2, isDir2]) =>\n isDir1 && !isDir2 ? -1 : isDir2 && !isDir1 ? 1 :\n name1.toLowerCase() < name2.toLowerCase() ? -1 : 0);\n for (i = 0; i < files.length; i++) {\n var [name, isDir] = files[i];\n let extension = name.replace(/^(?:(?!\\.(?=[^\\.]*$)).)*\\.?/, \"\");\n if(\".\" + extension == name || extension === \"\") extension = \"-\";\n const img_exts = [\"jpeg\", \"jpg\", \"png\", \"svg\", \"tiff\", \"tif\", \"gif\", \"pdf\"]\n const is_img = img_exts.includes(extension.toLowerCase());\n if (!is_img) {\n if (isDir) {\n form.append(fileItemDisplay(name, isDir))\n } else {\n form.append(fileItemDisplay(name, isDir));\n }\n } else {\n form.append(fileItemDisplay(name));\n }\n }\n\n form.append(el(\"input\", {type:\"file\", id:\"fileElem\", onchange:\"handleFiles(this.files)\"}, [], {}));\n }\n loadFileList();\n var goodReload = () => {\n document.getElementById(\"fileListing\").innerHTML = \"\";\n thisListDir = fullListDir (editor.config.path);\n loadFileList();\n }\n window.addEventListener('drop', handleDrop, false);\n window.addEventListener('dragover', (e) => e.preventDefault(), false);\n </script></body></html>]\n else \n Ok [<html><head></head><body class=\"editor-error\">\n <p>Editor cannot open file because it does not recognize the extension.</p>\n <p>As an alternative, you can open the file in raw mode by appending <code>?raw</code> to it.</p>\n <button onclick=\"\"\"\n location.search = location.search + (location.search == \"\" ? \"?raw\" : \"&raw\");\n \"\"\">Open @path in raw mode</button>\n </body></html>]\n\n{---------------------------------------------------------------------------\n Recovers from evaluation errors\n Recovers if page does not contain an html tag or a body tag\n----------------------------------------------------------------------------}\nrecoveredEvaluatedPage: List HtmlNode\nrecoveredEvaluatedPage = --updatecheckpoint \"recoveredEvaluatedPage\" <|\n case evaluatedPage of\n Err msg -> serverOwned \"Error Report\" <|\n [<html><head></head><body style=\"color:#cc0000\" class=\"editor-error\"><div style=\"max-width:600px;margin-left:auto;margin-right:auto\"><h1>Error report</h1><button onclick=\"editor.reload();\" title=\"Reload the current page\">Reload</button><pre style=\"white-space:pre-wrap\">@msg</pre></div></body></html>]\n Ok nodes ->\n let hasChildTag theTag nodes = List.any (case of [tag, _, _] -> tag == theTag; _ -> False) nodes\n recoverHtmlChildren nodes =\n let hasHead = hasChildTag \"head\" nodes\n hasBody = hasChildTag \"body\" nodes\n in\n if hasHead && hasBody then nodes else\n let startBodyIndex = List.indexWhere (\n case of\n [tag, _, _] -> tag /= \"title\" && tag /= \"link\" && tag /= \"meta\" && tag /= \"script\" && tag /= \"style\" && tag /= \"base\" && tag /= \"isindex\" && tag /= \"nextid\" && tag /= \"range\" && tag /= \"head\"\n [\"TEXT\", x] -> not (Regex.matchIn \"\"\"^\\s*$\"\"\" x)\n [\"COMMENT\", _] -> False\n ) nodes in\n case List.split startBodyIndex nodes of\n (([\"head\", _, _] as head)::whitespace, bodyElems) -> head :: (whitespace ++ [[\"body\", [], bodyElems]])\n (headElems, ([\"body\", _, _] as body) :: whitespace) -> [[\"head\", [], headElems], body]\n (headElems, bodyElems) -> [[\"head\", [], headElems], [\"body\", [], bodyElems]]\n recoverHtml nodes = nodes |> List.mapWithReverse identity (case of\n [\"html\", attrs, children] -> [\"html\", attrs, recoverHtmlChildren children]\n x -> x\n )\n in\n if hasChildTag \"html\" nodes then\n recoverHtml nodes\n else -- We need to wrap nodes with html, title, links, consecutive style and script and empty text nodes\n let aux nodes = case nodes of\n ([\"TEXT\", x] as head) :: rest ->\n if Regex.matchIn \"\"\"^\\s*$\"\"\" x then\n head :: aux rest\n else recoverHtml [[\"html\", [], nodes]]\n ([\"COMMENT\", x] as head) :: rest ->\n head :: aux rest\n ((tag :: _ :: _ :: _) as head) :: rest ->\n if tag == \"!DOCTYPE\" then\n head :: aux rest\n else recoverHtml [[\"html\", [], nodes]]\n in aux nodes\n\njsEnabled = boolVar \"js\" True\n\nremoveJS node = case node of\n [text, content] -> node\n [tag, attrs, children] ->\n if tag == \"script\" then [tag, [], [[\"TEXT\", \"/*Script disabled by Thaditor*/\"]]] else\n [tag, attrs, List.map removeJS children]\n _ -> []\n\n{-\n-- On reverse, all ghost elems will have disappeared. We reinsert them.\nprependGhosts ghostElems = update.lens {\n apply elems = ghostElems ++ elems\n update _ = Ok (InputsWithDiffs [(elems, VListDiffs [(0, ListElemInsert)\n} finalElems\n-}\n\n{---------------------------------------------------------------------------\n Instruments the resulting HTML page\n - Removes whitespace that are siblings of <head> and <body>\n - !f the page is editable:\n * Adds the contenteditable attribute to body\n * Adds the edition menu and the source preview area\n - Else: Adds the \"edit\" box to switch to edit mode\n - Adds the initial scripts\n - Append the edition scripts so that we can modify the page even without edit mode (that's dangerous, should we avoid this?)\n----------------------------------------------------------------------------}\nmain: List HtmlNode\nmain =\n --updatecheckpoint \"main\" <|\n let filteredMainPage = List.filter (case of -- Remove text nodes from top-level document.\n [\"TEXT\", _] -> False\n _ -