From bf4d7c28ddcc77ca0be3622336306cfc776e54fb Mon Sep 17 00:00:00 2001 From: Conrad Zelck <git@simpel.cc> Date: Fri, 6 Oct 2023 17:09:12 +0200 Subject: [PATCH] feat: introduce input file upload Signed-off-by: Conrad Zelck <git@simpel.cc> --- index.php | 26 ++++++------ pandoc.php | 14 +++++-- script.js | 116 +++++++++++++++++++++++++++++++++++------------------ style.css | 40 +++++++++++++++++- 4 files changed, 137 insertions(+), 59 deletions(-) diff --git a/index.php b/index.php index f6b4213..45c0d93 100644 --- a/index.php +++ b/index.php @@ -17,8 +17,8 @@ </header> <main> - <dialog> - <p id="errorMessageText">Error message here</p> + <dialog id="dialog"> + <p id="dialogText">Message here</p> <button onclick="dialog.close()">Close</button> </dialog> <div class="first-in-main two-column"> @@ -137,7 +137,7 @@ </select> </div> </div> - <details open> + <details id="options"> <summary>Options</summary> <div id="details"> <div id="checkboxes"> @@ -186,18 +186,17 @@ <div id="files" class="two-column"> <div class="left"> <label for="cb-inputfile" title="Use a file as input"> - <input type="checkbox" id="cb-inputfile" name="cb-inputfile" onchange="checkInputFile()">Use file as input</label> + <input type="checkbox" id="cb-inputfile" name="cb-inputfile" onchange="checkInputFile(),pandoc()">Use file as input</label> <form id="inputfile-form" class="file" action="/input" method="post" enctype="multipart/form-data"> <label for="inputfile">File <input id="inputfile" name="file" type="file" /></label> - <button id="upload-button">Upload</button> </form> </div> <div class="right"> <label for="cb-outputfile" title="Use a file for output"> - <input type="checkbox" id="cb-outputfile" name="cb-outputfile" onchange="checkOutputFile()">Use file for output</label> + <input type="checkbox" id="cb-outputfile" name="cb-outputfile" onchange="checkOutputFile(),pandoc()">Use file for output</label> <div class="download"> - <a download="test.txt" href='#' id="download">Download the output file</a> + <a id="download"></a> </div> </div> </div> @@ -206,11 +205,11 @@ <div class="two-column"> <div class="left"> <div class="flex-space-between"> - <label for="input" class="labelLikeHeading">Input field</label> + <label for="input" id="label-inputfield" class="labelLikeHeading">Input field</label> <button type="button" id="convert" name="convert" title="Convert input. You can use this shortcut: OSX: [ Ctrl ] + [ Opt ] + [ p ] -WIN: [ Alt ] + [ p ]" accesskey="p" onclick="pandoc(true)">Convert input [p]</button> +WIN: [ Alt ] + [ p ]" accesskey="p" onclick="pandoc(true)">Convert [p]</button> </div> <div id="inputDub" class="grow-wrap"> <textarea id="input" name="input"></textarea> @@ -218,7 +217,7 @@ WIN: [ Alt ] + [ p ]" accesskey="p" onclick="pandoc(true)">Convert input [p]</bu </div> <div class="right"> <div class="flex-space-between"> - <p class="label">Output field</p> + <p id="label-outputfield" class="label">Output field</p> <button type="button" id="copy" name="copy" title="Copy output text. You can use this shortcut: OSX: [ Ctrl ] + [ Opt ] + [ c ] @@ -234,10 +233,9 @@ WIN: [ Alt ] + [ c ]" accesskey="c" onclick="copyOutput()">Copy output field [c] inputField.addEventListener("input", adaptTextareaSize()); inputField.addEventListener("change", adaptTextareaSize()); // Error messages dialog - const dialog = document.querySelector("dialog"); - const closeButton = document.querySelector("dialog button"); - // "Close" button closes the dialog - closeButton.addEventListener("click", () => { + const dialog = document.getElementById("dialog"); + const closeButtonDialog = document.querySelector("dialog button"); + closeButtonDialog.addEventListener("click", () => { dialog.close(); }); // disable form for input file diff --git a/pandoc.php b/pandoc.php index 6bd0b10..d8765fc 100644 --- a/pandoc.php +++ b/pandoc.php @@ -8,18 +8,24 @@ $debug = true; } - // DEBUG: output all set variables from $_POST + // DEBUG: output all set variables from $_POST and $_FILES if ($debug) { var_dump($_POST); + var_dump($_FILES); echo '================================================== '; } // give input file a name that shouldn't collide with other users $timestamp = microtime(true); - $inputFile = 'input/input' . $timestamp . '.txt'; - // always use a file instead a string from stdin (because of security and special characters like ') - file_put_contents($inputFile, $_POST['input']); + if ($_POST['useInputFile'] == "true") { + $inputFile = 'input/input' . $timestamp . '.' . $_POST['inputFileExtension']; + move_uploaded_file($_FILES['inputFile']['tmp_name'], $inputFile); + } else { + $inputFile = 'input/input' . $timestamp . '.txt'; + // always use a file instead a string from stdin (because of security and special characters like ') + file_put_contents($inputFile, $_POST['input']); + } // run pandoc in a sandbox, limiting IO operations in readers and writers to reading the files specified on the command line. $command = 'pandoc --sandbox'; diff --git a/script.js b/script.js index d0a9518..e65dd25 100644 --- a/script.js +++ b/script.js @@ -2,7 +2,7 @@ const urlHost = window.location.href.substr(0, window.location.href.lastIndexOf( // the following object keeps the file extension for the select option values of 'from' and 'to' // access: value = extension["key"] -const extension = {"asciidoc_legacy": "asciidoc", "asciidoc": "asciidoc", "beamer": "tex", "biblatex": "bib", "bibtex": "bibtex", "chunkedhtml": "zip", "commonmark_x": "md", "commonmark": "md", "context": "tex", "creole": "txt", "csljson": "json", "csv": "csv", "docbook5": "xml", "docbook": "xml", "docx": "docx", "dokuwiki": "txt", "dzslides": "html", "endnotexml": "xml", "epub2": "epub", "epub3": "epub", "fb2": "fb2", "gfm": "md", "haddock": "md", "html4": "html", "html5": "html", "html": "html", "icml": "icml", "ipynb": "ipynb", "jats_archiving": "xml", "jats_articleauthoring": "xml", "jats_publishing": "xml", "jats": "xml", "jira": "txt", "json": "json", "latex": "tex", "man": "man", "markdown_mmd": "md", "markdown_phpextra": "md", "markdown_strict": "md", "markdown": "md", "markua": "md", "mediawiki": "txt", "ms": "ms", "muse": "txt", "native": "hs", "odt": "odt", "opendocument": "odf", "opml": "xml", "org": "txt", "pdf": "pdf", "plain": "txt", "pptx": "pptx", "preview": "html", "revealjs": "html", "ris": "ris", "rst": "rst", "rtf": "rtf", "s5": "html", "slideous": "html", "slidy": "html", "t2t": "t2t", "tei": "tei", "texinfo": "texi", "textile": "textile", "tikiwiki": "txt", "tsv": "tsv", "twiki": "txt", "typst": "typ", "vimwiki": "txt", "xwiki": "txt", "zimwiki": "txt"}; +const extension = {"asciidoc_legacy": "asciidoc", "asciidoc": "asciidoc", "beamer": "tex", "biblatex": "bib", "bibtex": "bibtex", "chunkedhtml": "zip", "commonmark_x": "md", "commonmark": "md", "context": "tex", "creole": "txt", "csljson": "json", "csv": "csv", "docbook5": "xml", "docbook": "xml", "docx": "docx", "dokuwiki": "txt", "dzslides": "html", "endnotexml": "xml", "epub2": "epub", "epub3": "epub", "epub": "epub", "fb2": "fb2", "gfm": "md", "haddock": "md", "html4": "html", "html5": "html", "html": "html", "icml": "icml", "ipynb": "ipynb", "jats_archiving": "xml", "jats_articleauthoring": "xml", "jats_publishing": "xml", "jats": "xml", "jira": "txt", "json": "json", "latex": "tex", "man": "man", "markdown_mmd": "md", "markdown_phpextra": "md", "markdown_strict": "md", "markdown": "md", "markua": "md", "mediawiki": "txt", "ms": "ms", "muse": "txt", "native": "hs", "odt": "odt", "opendocument": "odf", "opml": "xml", "org": "txt", "pdf": "pdf", "plain": "txt", "pptx": "pptx", "preview": "html", "revealjs": "html", "ris": "ris", "rst": "rst", "rtf": "rtf", "s5": "html", "slideous": "html", "slidy": "html", "t2t": "t2t", "tei": "tei", "texinfo": "texi", "textile": "textile", "tikiwiki": "txt", "tsv": "tsv", "twiki": "txt", "typst": "typ", "vimwiki": "txt", "xwiki": "txt", "zimwiki": "txt"}; function pandoc(alert) { var url = urlHost + '/pandoc.php'; @@ -20,48 +20,67 @@ function pandoc(alert) { var htmlMathMethod = document.getElementById('html-math-method').value; // files var useInputFile = document.getElementById('cb-inputfile').checked; - console.log(document.getElementById('from').value); var inputFileExtension = extension[document.getElementById('from').value]; - console.log(inputFileExtension); + // docx, epub or odt must be dilivered as a file + if (!useInputFile && (inputFileExtension == 'docx' || inputFileExtension == 'epub' || inputFileExtension == 'odt')) { + if (alert === true) { + document.getElementById('options').setAttribute("open", "open"); + document.getElementById('cb-inputfile').checked = true; + checkInputFile(); + pushDialog("Error: For conversion from '" + inputFileExtension + "' you must select a file as input.", "error", "inputfile"); + } + return; + } + var inputFile = false; + if (useInputFile) { + inputFile = document.getElementById('inputfile').files[0]; + } + if (!inputFile && useInputFile) { + if (alert === true) { + pushDialog("Error: Nothing to convert.\nYou must select a file as input.", "error", "inputfile"); + } + return; + } var useOutputFile = document.getElementById('cb-outputfile').checked; - console.log(document.getElementById('to').value); var outputFileExtension = extension[document.getElementById('to').value]; - console.log(outputFileExtension); // content var from = document.getElementById('from').value; if (from === "none") { if (alert === true) { - pushErrorMessage("Error: You must select an input format in the 'From' pulldown menu.", "from"); + pushDialog("Error: You must select an input format in the 'From' pulldown menu.", "error", "from"); } return; } var to = document.getElementById('to').value; var input = document.getElementById('input').value; - if (isEmpty(input)) { + if (isEmpty(input) && !useInputFile) { if (alert === true) { - pushErrorMessage("Error: You must give some input to convert.", "input"); + pushDialog("Error: Nothing to convert.\n\nYou must either write something into the 'Input field' or select a file as input in the 'Options'.", "error"); } return; } var formData = new FormData(); // checkboxes - formData.append('standalone', standalone); - formData.append('tableOfContents', tableOfContents); - formData.append('numberSections', numberSections); - formData.append('citeproc', citeproc); + formData.set('standalone', standalone); + formData.set('tableOfContents', tableOfContents); + formData.set('numberSections', numberSections); + formData.set('citeproc', citeproc); // selects - formData.append('wrap', wrap); - formData.append('highlightStyle', highlightStyle); - formData.append('htmlMathMethod', htmlMathMethod); + formData.set('wrap', wrap); + formData.set('highlightStyle', highlightStyle); + formData.set('htmlMathMethod', htmlMathMethod); // files - formData.append('useInputFile', useInputFile); - formData.append('inputFileExtension', inputFileExtension); - formData.append('useOutputFile', useOutputFile); - formData.append('outputFileExtension', outputFileExtension); + formData.set('useInputFile', useInputFile); + formData.set('inputFileExtension', inputFileExtension); + formData.set('inputFile', inputFile); + formData.set('useOutputFile', useOutputFile); + formData.set('outputFileExtension', outputFileExtension); // content - formData.append('from', from); - formData.append('to', to); - formData.append('input', input); + formData.set('from', from); + formData.set('to', to); + formData.set('input', input); + + pushDialog('Converting with pandoc', 'busy'); fetch(url, { method: 'POST', @@ -71,7 +90,6 @@ function pandoc(alert) { if (!response.ok) { throw new Error("HTTP error " + response.status); } - console.log('response'); if (useOutputFile) { return response.blob(); } else { @@ -79,7 +97,6 @@ function pandoc(alert) { } }) .then(content => { - console.log('context'); if (useOutputFile) { // output as file let blob = new Blob([content], {type: 'text/plain'}); @@ -87,12 +104,11 @@ function pandoc(alert) { download.href = URL.createObjectURL(blob); let timestamp = new Date().toISOString().replaceAll('T','_').replaceAll(':', '-').slice(0, 19); download.setAttribute("download", "output_" + timestamp + "." + outputFileExtension); - // set a notice on the output field - document.getElementById("output").innerText = "Use download link above to download 'output_" + timestamp + "." + outputFileExtension + "'."; + document.getElementById("download").innerText = "Download output_" + timestamp + "." + outputFileExtension; + // close the busy dialog + dialog.close() } else { - // console.log(text); if (to === "preview") { - // console.log("preview"); document.getElementById("output").innerHTML = content; } else { // delete all elements contents @@ -101,11 +117,15 @@ function pandoc(alert) { document.getElementById("output").appendChild(node); node.innerText = content; } + // close the busy dialog + dialog.close() } - console.log('end of fetch'); }) .catch(error => { - console.log('Error fetching pandoc output: ' + error); + // close the busy dialog + dialog.close() + pushDialog('Error fetching pandoc output: ' + error, 'error') + console.log('Error fetching pandoc output: ' + error); }); } @@ -126,18 +146,28 @@ function toggleToc() { function checkInputFile() { if (document.getElementById('cb-inputfile').checked === true) { document.getElementById('inputfile').removeAttribute("disabled"); - document.getElementById('upload-button').removeAttribute("disabled"); + document.getElementById('input').setAttribute("disabled", "disabled"); + document.getElementById('label-inputfield').classList.add("disabled"); } else { document.getElementById('inputfile').setAttribute("disabled", "disabled"); - document.getElementById('upload-button').setAttribute("disabled", "disabled"); + document.getElementById('input').removeAttribute("disabled"); + document.getElementById('label-inputfield').classList.remove("disabled"); } } function checkOutputFile() { if (document.getElementById('cb-outputfile').checked === true) { document.getElementById('download').setAttribute("href", "#"); + document.getElementById('copy').setAttribute("disabled", "disabled"); + document.getElementById('output').classList.add("disabled"); + document.getElementById('output').innerText = ""; + document.getElementById('label-outputfield').classList.add("disabled"); } else { document.getElementById('download').removeAttribute("href"); + document.getElementById("download").innerText = ""; + document.getElementById('copy').removeAttribute("disabled"); + document.getElementById('output').classList.remove("disabled"); + document.getElementById('label-outputfield').classList.remove("disabled"); } } @@ -173,12 +203,19 @@ function adaptTextareaSize() { }); } -function pushErrorMessage(text, elementById) { - var errorMessageText = document.getElementById('errorMessageText'); - errorMessageText.innerText = text; - errorMessageText.role = 'alert'; - if (typeof elementById !== 'undefined') { - document.getElementById(elementById).focus(); +function pushDialog(text, type, elementById) { + var dialogText = document.getElementById('dialogText'); + dialogText.innerText = text; + if (type === "error") { + document.getElementById('dialog').classList.add('error-dialog'); + document.getElementById('dialog').classList.remove('busy-dialog'); + dialogText.role = 'alert'; + if (typeof elementById !== 'undefined') { + document.getElementById(elementById).focus(); + } + } else if (type === "busy") { + document.getElementById('dialog').classList.remove('error-dialog'); + document.getElementById('dialog').classList.add('busy-dialog'); } dialog.showModal(); } @@ -189,10 +226,11 @@ function isEmpty(string) { }; function useExample() { - // console.log(example); var inputField = document.getElementById("input"); inputField.value = example; document.getElementById('from').value = 'markdown'; + document.getElementById('cb-inputfile').checked = false; + checkInputFile(); // fire onInput event to adapt height of textarea var eventInput = new Event('input', { bubbles: true }); inputField.dispatchEvent(eventInput); diff --git a/style.css b/style.css index 0abd20d..b09ecdc 100644 --- a/style.css +++ b/style.css @@ -44,17 +44,42 @@ body { dialog { color: white; +} + +dialog:focus { + outline: none; +} + +dialog::backdrop { + background: rgba(85, 85, 85, 0.80); +} + +.error-dialog { + color: white; background-color: #AE0000; } +.busy-dialog { + color: white; + background-color: #006900; +} + dialog p { margin-top: 0; } +dialog.busy-dialog p { + margin-bottom: 0; +} + dialog button { float: right; } +dialog.busy-dialog button { + display: none; +} + a { color: #ffffff; } @@ -169,6 +194,7 @@ p.label { margin:0; font-size: 1.2em; display: inline; + pointer-events: none; } /* select shall fill the rest of the column right from label */ @@ -241,10 +267,20 @@ input[type="file"] { background-color: inherit; } -button:disabled, input:disabled { +button:disabled, input:disabled, textarea:disabled { + background-color: inherit; + color: #AAAAAA; + border-color: #AAAAAA; + outline-color: #AAAAAA; + pointer-events: none; +} + +div.disabled, p.disabled, label.disabled, #output.disabled { background-color: inherit; color: #AAAAAA; - border-color: inherit; + border-color: #AAAAAA; + outline-color: #AAAAAA; + pointer-events: none; } label:has(> input:disabled) { -- GitLab