From 7bd7c02a86293888f6d62d1eb3bb88856fba0d94 Mon Sep 17 00:00:00 2001
From: Conrad Zelck <git@simpel.cc>
Date: Fri, 6 Oct 2023 02:00:36 +0200
Subject: [PATCH] feat: introduce output file download

Signed-off-by: Conrad Zelck <git@simpel.cc>
---
 index.php  | 26 ++++++++++++++-------
 pandoc.php | 34 ++++++++++++++++++++++------
 script.js  | 66 +++++++++++++++++++++++++++++++++++++++++++++++++-----
 style.css  | 23 +++++++++++++++++--
 4 files changed, 126 insertions(+), 23 deletions(-)

diff --git a/index.php b/index.php
index d24e0fd..f6b4213 100644
--- a/index.php
+++ b/index.php
@@ -76,7 +76,7 @@
           <option value="preview">Preview</option>
           <option value="jats">alias for jats_archiving</option>
           <option value="asciidoc">AsciiDoc (modern) as interpreted by AsciiDoctor</option>
-          <option value="asciidoc_legacy.">AsciiDoc as interpreted by asciidoc-py</option>
+          <option value="asciidoc_legacy">AsciiDoc as interpreted by asciidoc-py</option>
           <option value="biblatex">BibLaTeX bibliography</option>
           <option value="bibtex">BibTeX bibliography</option>
           <option value="context">ConTeXt</option>
@@ -183,17 +183,24 @@
           </label>
         </div>
         <input type="button" id="example" name="example" value="Use markdown example" onclick="useExample()">
-        <!-- <div id="files" class="two-column">
+        <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="useInputFile()">Use file as input</label>
-            <form class="file" action="/input" method="post" enctype="multipart/form-data">
-              <label for="inputfile">File</label>
-              <input id="inputfile" name="file" type="file" />
-              <button>Upload</button>
+              <input type="checkbox" id="cb-inputfile" name="cb-inputfile" onchange="checkInputFile()">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> -->
+          <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>
+            <div class="download">
+              <a download="test.txt" href='#' id="download">Download the output file</a>
+            </div>
+          </div>
+        </div>
       </div>
     </details>
     <div class="two-column">
@@ -233,6 +240,9 @@ WIN: [ Alt ] + [ c ]" accesskey="c" onclick="copyOutput()">Copy output field [c]
     closeButton.addEventListener("click", () => {
       dialog.close();
     });
+    // disable form for input file
+    checkInputFile();
+    checkOutputFile();
   </script>
 </body>
 
diff --git a/pandoc.php b/pandoc.php
index 644be77..6bd0b10 100644
--- a/pandoc.php
+++ b/pandoc.php
@@ -16,9 +16,10 @@
     }
 
     // give input file a name that shouldn't collide with other users
-    $file = 'input/input' . microtime(true) . '.txt';
+    $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($file, $_POST['input']);
+    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';
@@ -73,12 +74,21 @@
     // option 'preview' should be rendered in the gui so use HTML
     if ($_POST['to'] == "preview") {
       $command  .= ' --to=html5';
+    // pdf - see https://pandoc.org/MANUAL#context
+    // you need to have context installed - see https://wiki.contextgarden.net/Installation
+    } elseif ($_POST['to'] == "pdf") {
+      $command  .= ' --to=context+tagging -V pdfa=3a';
+      // pdf is only working in standalone mode
+      if ($_POST['standalone'] == "false") {$command .= ' --standalone';}
     } else {
       $command .= ' --to=' . $_POST['to'];
     }
-
+    // set output file if asked for
+    if ($_POST['useOutputFile'] == "true") {
+      $command .= ' -o output/output' . $timestamp . '.' . $_POST['outputFileExtension'];
+    }
     // always use a file instead a string from stdin (because of security and special characters like ')
-    $command .= ' ' . $file;
+    $command .= ' ' . $inputFile;
 
     // DEBUG: output error messages from cmd line
     if ($debug) {
@@ -100,8 +110,18 @@
     // execute pandoc
     $return = shell_exec($command);
     // delete input file as it is not needed anymore
-    unlink($file);
-    // put the output string back to the client
-    echo "$return";
+    if (file_exists($inputFile)) {
+      unlink($inputFile);
+    }
+
+    // return result
+    if ($_POST['useOutputFile'] == "false") {
+      // put the output string back to the client
+      echo "$return";
+    } else {
+      // return the file binary
+      readfile('output/output' . $timestamp . '.' . $_POST['outputFileExtension']);
+      unlink('output/output' . $timestamp . '.' . $_POST['outputFileExtension']);
+    }
   }
 ?>
diff --git a/script.js b/script.js
index e107ef7..d0a9518 100644
--- a/script.js
+++ b/script.js
@@ -1,5 +1,9 @@
 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"};
+
 function pandoc(alert) {
   var url = urlHost + '/pandoc.php';
   if (typeof alert === 'undefined') {
@@ -14,6 +18,15 @@ function pandoc(alert) {
   var wrap  = document.getElementById('wrap').value;
   var highlightStyle  = document.getElementById('highlight-style').value;
   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);
+  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") {
@@ -40,6 +53,11 @@ function pandoc(alert) {
   formData.append('wrap', wrap);
   formData.append('highlightStyle', highlightStyle);
   formData.append('htmlMathMethod', htmlMathMethod);
+  // files
+  formData.append('useInputFile', useInputFile);
+  formData.append('inputFileExtension', inputFileExtension);
+  formData.append('useOutputFile', useOutputFile);
+  formData.append('outputFileExtension', outputFileExtension);
   // content
   formData.append('from', from);
   formData.append('to', to);
@@ -50,23 +68,41 @@ function pandoc(alert) {
       body: formData
     })
     .then(response => {
-        if (!response.ok) {
-            throw new Error("HTTP error " + response.status);
-        }
+      if (!response.ok) {
+          throw new Error("HTTP error " + response.status);
+      }
+      console.log('response');
+      if (useOutputFile) {
+        return response.blob();
+      } else {
         return response.text();
+      }
     })
-    .then(text => {
+    .then(content => {
+      console.log('context');
+      if (useOutputFile) {
+        // output as file
+        let blob = new Blob([content], {type: 'text/plain'});
+        let download = document.getElementById('download');
+        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 + "'.";
+      } else {
         // console.log(text);
         if (to === "preview") {
           // console.log("preview");
-          document.getElementById("output").innerHTML = text;
+          document.getElementById("output").innerHTML = content;
         } else {
           // delete all elements contents
           document.getElementById("output").innerHTML = "";
           var node = document.createElement("pre");
           document.getElementById("output").appendChild(node);
-          node.innerText = text;
+          node.innerText = content;
         }
+      }
+      console.log('end of fetch');
     })
     .catch(error => {
         console.log('Error fetching pandoc output: ' + error);
@@ -87,6 +123,24 @@ function toggleToc() {
   }
 }
 
+function checkInputFile() {
+  if (document.getElementById('cb-inputfile').checked === true) {
+    document.getElementById('inputfile').removeAttribute("disabled");
+    document.getElementById('upload-button').removeAttribute("disabled");
+  } else {
+    document.getElementById('inputfile').setAttribute("disabled", "disabled");
+    document.getElementById('upload-button').setAttribute("disabled", "disabled");
+  }
+}
+
+function checkOutputFile() {
+  if (document.getElementById('cb-outputfile').checked === true) {
+    document.getElementById('download').setAttribute("href", "#");
+  } else {
+    document.getElementById('download').removeAttribute("href");
+  }
+}
+
 // This will return the raw HTML, but perhaps you want to do something different,
 // for example: recursively embed computed styles:
 // https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle
diff --git a/style.css b/style.css
index 1ab4153..0abd20d 100644
--- a/style.css
+++ b/style.css
@@ -57,11 +57,15 @@ dialog button {
 
 a {
   color: #ffffff;
+}
+
+header a {
   text-decoration: none;
 }
 
-#output a {
-  text-decoration: underline;
+/* kind of disabled link - text-decoration will be none automatically */
+a:not([href]) {
+  color: #AAAAAA;
 }
 
 header {
@@ -237,10 +241,25 @@ input[type="file"] {
   background-color: inherit;
 }
 
+button:disabled, input:disabled {
+  background-color: inherit;
+  color: #AAAAAA;
+  border-color: inherit;
+}
+
+label:has(> input:disabled) {
+  color: #AAAAAA;
+}
+
 form.file {
   padding-top: var(--default-top-padding);
 }
 
+div.download {
+  /* + 5px to align with the button in the left segment */
+  padding-top: calc(var(--default-top-padding) + 5px);
+}
+
 #example {
   margin-top: var(--default-top-padding);
 }
-- 
GitLab