From 91b31268de0d623ceeb2374c00988b6a9d3c08ad Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Sat, 3 May 2014 16:58:43 +0200 Subject: [PATCH 01/29] Update documentation. Add support to create Latex strings out of Equation objects. --- README.md | 2 ++ converttomarkdown.gapps | 51 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fba7771..8502e1d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen * bullet lists are converted to "`*`" Markdown format appropriately, including nested lists * Images: * images are correctly extracted and sent as attachments + * Equations: + * Equations are converted to LaTex equation strings and surrounded by ``$`` signs * Blocks: * Table of contents is replaced by `[[TOC]]` * blocks of text delimited by "--- class whateverclassnameyouwant" and "---" are converted to `
` diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index b6344de..6166705 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -68,8 +68,13 @@ function ConvertToMarkdown() { } + // Add markdown document to attachments attachments.push({"fileName":DocumentApp.getActiveDocument().getName()+".md", "mimeType": "text/plain", "content": text}); + // Log output for debugging + Logger.log(text); + + // Send email with markdown document MailApp.sendEmail(Session.getActiveUser().getEmail(), "[MARKDOWN_MAKER] "+DocumentApp.getActiveDocument().getName(), "Your converted markdown document is attached (converted from "+DocumentApp.getActiveDocument().getUrl()+")"+ @@ -151,6 +156,17 @@ function processParagraph(index, element, inSrc, imageCounter, listCounters) { textElements.push('* * *\n'); } else if (t === DocumentApp.ElementType.FOOTNOTE) { textElements.push(' (NOTE: '+element.getChild(i).getFootnoteContents().getText()+')'); + } else if (t == DocumentApp.ElementType.EQUATION) { + // Convert equation to Latex equation string + var equation = element.getChild(i); + var latexEquation = handleEquationFunction(equation); + + // Add equation marker + latexEquation = "$" + latexEquation.trim() + "$"; + + // Put equation onto stack + textElements.push(latexEquation); + } else { throw "Paragraph "+index+" of type "+element.getType()+" has an unsupported child: " +t+" "+(element.getChild(i)["getText"] ? element.getChild(i).getText():'')+" index="+index; @@ -197,6 +213,39 @@ function processParagraph(index, element, inSrc, imageCounter, listCounters) { return result; } +// Escape chars with a special meaning in Latex +function latexSanitize(text) { + text = text.replace("\\", "\\\\"); + text = text.replace("%", "\\%"); + return text; +} + +// Converte an Equation or Function element to a Latex expression +function handleEquationFunction(func) { + var equation = ""; + for(var i=0; i Date: Sat, 3 May 2014 17:09:05 +0200 Subject: [PATCH 02/29] Added me as a contributor. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8502e1d..6524a2e 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen * Renato Mangini - [G+](//google.com/+renatomangini) - [Github](//github.com/mangini) * Ed Bacher - [G+](//plus.google.com/106923847899206957842) - [Github](//github.com/evbacher) +* Andreas Wolke - [G+](//plus.google.com/+AndreasWolke) - [Github](//github.com/jacksonicson) ## LICENSE From 82cc41787db23c8f5a6248eb942621ae6984c33f Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Sat, 3 May 2014 20:53:15 +0200 Subject: [PATCH 03/29] Fixed error in equation generator. Removed EMail function. Script creates a new target directory 'target' in the same directory as the document file where everything gets stored. Added Google docs menu to execute Markdown converter without going through Scripts. Added support to convert equation under the cursor only. --- converttomarkdown.gapps | 166 +++++++++++++++++++++++++++++++++------- 1 file changed, 139 insertions(+), 27 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 6166705..310b49c 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -10,6 +10,33 @@ Usage: - Converted doc will be mailed to you. Subject will be "[MARKDOWN_MAKER]...". */ +// Open handler to add Menu +function onOpen() { + var ui = DocumentApp.getUi(); + ui.createMenu('Markdown') + .addItem('Document', 'ConvertToMarkdown') + .addItem('Equation', 'ConvertEquation') + .addToUi(); +} + +function ConvertEquation() { + var element = DocumentApp.getActiveDocument().getCursor().getElement(); + element = element.getParent(); + + if(element.getType() != DocumentApp.ElementType.EQUATION) { + DocumentApp.getUi().alert("Put cursor into an equation element!"); + return; + } + + // Covert equation + var latexEquation = handleEquationFunction(element); + var latexEquationText = "$" + latexEquation.trim() + "$"; + + // Show results + DocumentApp.getUi().alert(latexEquationText); +} + +// Covert current document to markdown function ConvertToMarkdown() { var numChildren = DocumentApp.getActiveDocument().getActiveSection().getNumChildren(); var text = ""; @@ -17,10 +44,12 @@ function ConvertToMarkdown() { var inClass = false; var globalImageCounter = 0; var globalListCounters = {}; + // edbacher: added a variable for indent in src
 block. Let style sheet do margin.
   var srcIndent = "";
   
   var attachments = [];
+  var files = []; 
   
   // Walk through all the child elements of the doc.
   for (var i = 0; i < numChildren; i++) {
@@ -54,12 +83,19 @@ function ConvertToMarkdown() {
         text+=result.text+"\n\n";
       }
       
+      
       if (result.images && result.images.length>0) {
         for (var j=0; j 1)
+  {
+    Logger.log("File has multiple parent directory. Script does not work in this case"); 
+    return null; 
+  }
+  
+  var parent = parents[0];
+  
+  // Check if target folder exists  
+  for(var folder in parent.getFolders()) {
+    folder = parent.getFolders()[folder]; 
+    
+    if(folder.getName() == 'target') {
+      Logger.log("Trashing target folder..."); 
+      folder.setTrashed(true); 
+      break; 
+    }
+  }
+  
+  // Create new target folder if none exists
+  Logger.log("Creating output folder..."); 
+  var found = parent.createFolder("target"); 
+  
+  // Write all files to target folder
+  for(var file in files) {
+    file = files[file];
+    var blob = file.blob.copyBlob();
+    var name = file.name; 
+    blob.setName(name); 
+    found.createFile(blob); 
+  }
+  
+  // Write mardown file to target folder
+  found.createFile(DocumentApp.getActiveDocument().getName() + ".md", text, "text/plain"); 
   
-  // Log output for debugging
+  // Debug logging
   Logger.log(text);
-   
-  // Send email with markdown document
-  MailApp.sendEmail(Session.getActiveUser().getEmail(), 
-                    "[MARKDOWN_MAKER] "+DocumentApp.getActiveDocument().getName(), 
-                    "Your converted markdown document is attached (converted from "+DocumentApp.getActiveDocument().getUrl()+")"+
-                    "\n\nDon't know how to use the format options? See http://github.com/mangini/gdocs2md\n",
-                    { "attachments": attachments });
 }
 
 function escapeHTML(text) {
   return text.replace(//g, '>');
 }
 
+function processTable(element, textElements) {
+  String.prototype.repeat = function( num )
+  {
+    return new Array( num + 1 ).join( this );
+  }
+  
+  textElements.push("\n");
+  
+  function buildTable(size) {
+    var stack = []
+    var maxSize = 0; 
+    
+    for(var ir=0; ir text.length) {
+          text += " ".repeat(size - text.length)
+        }
+        
+        stack.push("| " + text);
+      }
+      
+      stack.push(" |\n");
+    }
+    
+    stack.push("\n");
+    return {
+      maxSize : maxSize,
+      stack : stack,
+    };
+  }
+  
+  var table = buildTable(100); 
+  table = buildTable(Math.max(10, table.maxSize + 1)); 
+  textElements = textElements.concat(table.stack);
+  
+  textElements.push("\n");
+  return textElements;
+}
+
 // Process each child element (not just paragraphs).
 function processParagraph(index, element, inSrc, imageCounter, listCounters) {
   // First, check for things that require no processing.
@@ -103,21 +224,8 @@ function processParagraph(index, element, inSrc, imageCounter, listCounters) {
   var textElements = [];
   var imagePrefix = "image_";
   
-  // Handle Table elements. Pretty simple-minded now, but works for simple tables.
-  // Note that Markdown does not process within block-level HTML, so it probably 
-  // doesn't make sense to add markup within tables.
   if (element.getType() === DocumentApp.ElementType.TABLE) {
-    textElements.push("\n");
-    var nCols = element.getChild(0).getNumCells();
-    for (var i = 0; i < element.getNumChildren(); i++) {
-      textElements.push("  \n");
-      // process this row
-      for (var j = 0; j < nCols; j++) {
-        textElements.push("    \n");
-      }
-      textElements.push("  \n");
-    }
-    textElements.push("
" + element.getChild(i).getChild(j).getText() + "
\n"); + textElements = processTable(element, textElements); } // Process various types (ElementType). @@ -148,6 +256,7 @@ function processParagraph(index, element, inSrc, imageCounter, listCounters) { textElements.push('![image alt text]('+name+')'); result.images.push( { "bytes": element.getChild(i).getBlob().getBytes(), + "blob": element.getChild(i).getBlob(), "type": contentType, "name": name}); } else if (t === DocumentApp.ElementType.PAGE_BREAK) { @@ -162,7 +271,7 @@ function processParagraph(index, element, inSrc, imageCounter, listCounters) { var latexEquation = handleEquationFunction(equation); // Add equation marker - latexEquation = "$" + latexEquation.trim() + "$"; + latexEquation = "$$" + latexEquation.trim() + "$$"; // Put equation onto stack textElements.push(latexEquation); @@ -222,12 +331,14 @@ function latexSanitize(text) { // Converte an Equation or Function element to a Latex expression function handleEquationFunction(func) { + Logger.log("Equation converter handling: " + func.getType()); var equation = ""; + for(var i=0; i Date: Sat, 3 May 2014 21:17:57 +0200 Subject: [PATCH 04/29] Fixed error in recursion. Did not attach returned string. --- converttomarkdown.gapps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 6166705..989a6f0 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -227,7 +227,7 @@ function handleEquationFunction(func) { var child = func.getChild(i); if(child.getType() == DocumentApp.ElementType.EQUATION_FUNCTION) { - equation = child.getCode() + "{" + handleEquationFunction(child); + equation += child.getCode() + "{" + handleEquationFunction(child); } else if(child.getType() == DocumentApp.ElementType.EQUATION_FUNCTION_ARGUMENT_SEPARATOR) { equation = equation.trim() + "}{"; From 6403e161a38733093b3e76c9715972fda3677c15 Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Sat, 3 May 2014 21:31:35 +0200 Subject: [PATCH 05/29] Go upwards until the document root if scanning for an equation. --- converttomarkdown.gapps | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 310b49c..31ee3ee 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -21,7 +21,14 @@ function onOpen() { function ConvertEquation() { var element = DocumentApp.getActiveDocument().getCursor().getElement(); - element = element.getParent(); + + // Scan upwards for an equation + while(element.getType() != DocumentApp.ElementType.EQUATION) { + if(element.getParent() == null) + break; + + element = element.getParent(); + } if(element.getType() != DocumentApp.ElementType.EQUATION) { DocumentApp.getUi().alert("Put cursor into an equation element!"); From 9098ef89e6fdf1130c201caa0b28e529db925c7f Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Sun, 4 May 2014 02:39:11 +0200 Subject: [PATCH 06/29] No double line breaks between item lists. --- converttomarkdown.gapps | 43 +++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 31ee3ee..38844c4 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -86,6 +86,8 @@ function ConvertToMarkdown() { text+=result.text+"\n\n"; } else if (inSrc) { text+=(srcIndent+escapeHTML(result.text)+"\n"); + } else if(result.text && result.text.length>0 && result.singleLb) { + text+=result.text+"\n"; } else if (result.text && result.text.length>0) { text+=result.text+"\n\n"; } @@ -220,21 +222,25 @@ function processParagraph(index, element, inSrc, imageCounter, listCounters) { if (element.getNumChildren()==0) { return null; } + // Punt on TOC. if (element.getType() === DocumentApp.ElementType.TABLE_OF_CONTENTS) { return {"text": "[[TOC]]"}; } // Set up for real results. - var result = {}; + var result = { + 'singleLb' : false, + }; var pOut = ""; var textElements = []; var imagePrefix = "image_"; - + + // Handle tables if (element.getType() === DocumentApp.ElementType.TABLE) { textElements = processTable(element, textElements); } - + // Process various types (ElementType). for (var i = 0; i < element.getNumChildren(); i++) { var t=element.getChild(i).getType(); @@ -312,9 +318,15 @@ function processParagraph(index, element, inSrc, imageCounter, listCounters) { 'src="http://www.html5rocks.com/static/jsperfview/embed.html?id='+RegExp.$1+ '">'; } else { - - prefix = findPrefix(inSrc, element, listCounters); + // Prefix for headlines and lists + prefix = findPrefix(index, inSrc, element, listCounters); + var singleLb = prefix.singleLb; + prefix = prefix.prefix; + Logger.log("single lb: " + singleLb); + result.singleLb = singleLb; + + // Join all text elements and add formatting var pOut = ""; for (var i=0; i): @@ -401,9 +414,23 @@ function findPrefix(inSrc, element, listCounters) { listCounters[key] = counter; prefix += counter+". "; } + + // Check if this item is followed by another list item + var num = DocumentApp.getActiveDocument().getActiveSection().getNumChildren(); + if(index + 1 < num) { + var next = DocumentApp.getActiveDocument().getActiveSection().getChild(index + 1); + if(next.getType() == DocumentApp.ElementType.LIST_ITEM) { + singleLineBreak = true; + } + } + } } - return prefix; + + return { + 'prefix' : prefix, + 'singleLb' : singleLineBreak + }; } function processTextElement(inSrc, txt) { From 2ec4cc4ca0282ae22bfb2c9a9d4f2cb87d85b673 Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Fri, 9 May 2014 15:03:31 +0200 Subject: [PATCH 07/29] Reintegrated support to send converted MD as an email. --- converttomarkdown.gapps | 116 +++++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 38844c4..48ad039 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -13,8 +13,9 @@ Usage: // Open handler to add Menu function onOpen() { var ui = DocumentApp.getUi(); - ui.createMenu('Markdown') - .addItem('Document', 'ConvertToMarkdown') + ui.createMenu('Export MD') + .addItem('File', 'ConvertToMarkdownFile') + .addItem('Email', 'ConvertToMarkdownEmail') .addItem('Equation', 'ConvertEquation') .addToUi(); } @@ -43,6 +44,71 @@ function ConvertEquation() { DocumentApp.getUi().alert(latexEquationText); } +// Convert current document to markdown and email it +function ConvertToMarkdownEmail() { + // Convert to markdown + var convertedDoc = ConvertToMarkdown(); + + // Add markdown document to attachments + convertedDoc.attachments.push({"fileName":DocumentApp.getActiveDocument().getName()+".md", + "mimeType": "text/plain", "content": convertedDoc.text}); + + + // Send email with markdown document + MailApp.sendEmail(Session.getActiveUser().getEmail(), + "[MARKDOWN_MAKER] "+DocumentApp.getActiveDocument().getName(), + "Your converted markdown document is attached (converted from "+DocumentApp.getActiveDocument().getUrl()+")"+ + "\n\nDon't know how to use the format options? See http://github.com/mangini/gdocs2md\n", + { "attachments": convertedDoc.attachments }); +} + +// Convert current document to file and save it to GDrive +function ConvertToMarkdownFile() { + // Convert to markdwon + var convertedDoc = ConvertToMarkdown(); + + // Create folder + var id = DocumentApp.getActiveDocument().getId(); + var file = DocsList.getFileById(id); + var parents = file.getParents(); + + if(parents.length > 1) + { + Logger.log("File has multiple parent directory. Script does not work in this case"); + return null; + } + + // Use first parent + var parent = parents[0]; + + // Check if target folder exists + for(var folder in parent.getFolders()) { + folder = parent.getFolders()[folder]; + + if(folder.getName() == 'target') { + Logger.log("Trashing target folder..."); + folder.setTrashed(true); + break; + } + } + + // Create new target folder if none exists + Logger.log("Creating output folder..."); + var found = parent.createFolder("target"); + + // Write all files to target folder + for(var file in convertedDoc.files) { + file = convertedDoc.files[file]; + var blob = file.blob.copyBlob(); + var name = file.name; + blob.setName(name); + found.createFile(blob); + } + + // Write mardown file to target folder + found.createFile(DocumentApp.getActiveDocument().getName() + ".md", convertedDoc.text, "text/plain"); +} + // Covert current document to markdown function ConvertToMarkdown() { var numChildren = DocumentApp.getActiveDocument().getActiveSection().getNumChildren(); @@ -113,48 +179,14 @@ function ConvertToMarkdown() { } - // Create folder - var id = DocumentApp.getActiveDocument().getId(); - var file = DocsList.getFileById(id); - var parents = file.getParents(); - - if(parents.length > 1) - { - Logger.log("File has multiple parent directory. Script does not work in this case"); - return null; - } - - var parent = parents[0]; - - // Check if target folder exists - for(var folder in parent.getFolders()) { - folder = parent.getFolders()[folder]; - - if(folder.getName() == 'target') { - Logger.log("Trashing target folder..."); - folder.setTrashed(true); - break; - } - } - - // Create new target folder if none exists - Logger.log("Creating output folder..."); - var found = parent.createFolder("target"); - - // Write all files to target folder - for(var file in files) { - file = files[file]; - var blob = file.blob.copyBlob(); - var name = file.name; - blob.setName(name); - found.createFile(blob); - } - - // Write mardown file to target folder - found.createFile(DocumentApp.getActiveDocument().getName() + ".md", text, "text/plain"); - // Debug logging Logger.log(text); + + return { + 'files' : files, + 'attachments' : attachments, + 'text' : text + }; } function escapeHTML(text) { From 5a38ec2f2fb37284148265f115a0218f35840c6d Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Fri, 9 May 2014 15:20:18 +0200 Subject: [PATCH 08/29] Changed menu naming --- converttomarkdown.gapps | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 48ad039..36ba3dc 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -13,11 +13,13 @@ Usage: // Open handler to add Menu function onOpen() { var ui = DocumentApp.getUi(); - ui.createMenu('Export MD') - .addItem('File', 'ConvertToMarkdownFile') - .addItem('Email', 'ConvertToMarkdownEmail') - .addItem('Equation', 'ConvertEquation') + ui.createMenu('Export') + .addItem('Markdown file', 'ConvertToMarkdownFile') + .addItem('Markdown email', 'ConvertToMarkdownEmail') + .addItem('Latex equation', 'ConvertEquation') .addToUi(); + + DocumentApp.getActiveDocument().add } function ConvertEquation() { From 209897e25e0946ebed48e03ad346d0d61e8b9263 Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Fri, 9 May 2014 15:22:26 +0200 Subject: [PATCH 09/29] Update README.md --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 6524a2e..16c91f6 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,7 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen * File -> Save * Running the script (run as many times as you want): - - Tools > Script Manager - - Select "ConvertToMarkdown" function. - - Click Run button (First run will require you to authorize it. Authorize and run again) - - Converted doc with images attached will be emailed to you. Subject will be "[MARKDOWN_MAKER]...". + - Export > Markdown email ## Interpreted formats From 47d0c8e76714b999cde160b2153cf4f2ae2f2d78 Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Fri, 9 May 2014 15:23:01 +0200 Subject: [PATCH 10/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16c91f6..ccd7217 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen * Open your Google Drive document (http://drive.google.com) * Tools -> Script Manager > New * Select "Blank Project", then paste this code in and save. - * Clear the myFunction() default empty function and paste the contents of [converttomarkdown.gapps](https://raw.github.com/mangini/gdocs2md/master/converttomarkdown.gapps) into the code editor + * Clear the myFunction() default empty function and paste the contents of `converttomarkdown.gapps` into the code editor * File -> Save * Running the script (run as many times as you want): From 5d867f58159bc83930064a31f3f1e09c39ffc1e5 Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Fri, 9 May 2014 15:32:07 +0200 Subject: [PATCH 11/29] Change table header separator from = to - --- converttomarkdown.gapps | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 36ba3dc..e417cd1 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -213,9 +213,9 @@ function processTable(element, textElements) { // Add header seperator if(ir == 1) { for(var ic=0; ic Date: Fri, 9 May 2014 18:09:56 +0200 Subject: [PATCH 12/29] Redesigned core functionality in a recursive approach. --- converttomarkdown.gapps | 306 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 300 insertions(+), 6 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index e417cd1..7455cdb 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -67,7 +67,7 @@ function ConvertToMarkdownEmail() { // Convert current document to file and save it to GDrive function ConvertToMarkdownFile() { // Convert to markdwon - var convertedDoc = ConvertToMarkdown(); + var convertedDoc = markdown(); // Create folder var id = DocumentApp.getActiveDocument().getId(); @@ -111,6 +111,51 @@ function ConvertToMarkdownFile() { found.createFile(DocumentApp.getActiveDocument().getName() + ".md", convertedDoc.text, "text/plain"); } +function markdown() { + var doc = DocumentApp.getActiveDocument().getActiveSection(); + + var state = { + 'inSource' : false, + 'images' : [], + 'imageCounter' : 0, + 'prevDoc' : [], + 'nextDoc' : [], + }; + var textElements = processDocument(doc, state, 0); + var text = textElements.join(''); + + // Replace critical chars + text = text.replace('\u201d', '"').replace('\u201c', '"'); + + // Debug logging + Logger.log("Result: " + text); + Logger.log("Images: " + state.imageCounter); + + // Build attachment and file lists + var attachments = []; + var files = []; + for(var i in state.images) { + var image = state.images[i]; + attachments.push( { + "fileName": image.name, + "mimeType": image.type, + "content": image.bytes + } ); + + files.push( { + "name" : image.name, + "blob" : image.blob + }); + } + + // Results + return { + 'files' : files, + 'attachments' : attachments, + 'text' : text, + }; +} + // Covert current document to markdown function ConvertToMarkdown() { var numChildren = DocumentApp.getActiveDocument().getActiveSection().getNumChildren(); @@ -195,7 +240,9 @@ function escapeHTML(text) { return text.replace(//g, '>'); } -function processTable(element, textElements) { +function processTable(element) { + var textElements = []; + String.prototype.repeat = function( num ) { return new Array( num + 1 ).join( this ); @@ -250,6 +297,238 @@ function processTable(element, textElements) { return textElements; } + +function handleText(doc, state) { + var formatted = doc.getText(); + var lastIndex = formatted.length; + var attrs = doc.getTextAttributeIndices(); + + // Iterate backwards through all attributes + for(var i=attrs.length-1; i >= 0; i--) { + // Current position in text + var index = attrs[i]; + + // Handle links + if(doc.getLinkUrl(index)) { + var url = doc.getLinkUrl(index); + if (i > 0 && attrs[i-1] == index - 1 && doc.getLinkUrl(attrs[i-1]) === url) { + i -= 1; + index = attrs[i]; + url = txt.getLinkUrl(off); + } + formatted = formatted.substring(0, index) + '[' + formatted.substring(index, lastIndex) + '](' + url + ')' + formatted.substring(lastIndex); + } + + // Handle font family + if(doc.getFontFamily(index)) { + var font = doc.getFontFamily(index); + var sourceFont = font.COURIER_NEW; + + if (!state.inSource && font === sourceFont) { + // Scan left until text without source font is found + while (i > 0 && txt.getFontFamily(attrs[i-1]) && txt.getFontFamily(attrs[i-1]) === sourceFont) { + i -= 1; + off = attrs[i]; + } + + formatted = formatted.substring(0, lastIndex) + '`' + formatted.substring(index, lastIndex) + '`' + formatted.substring(lastIndex); + } + } + + // Handle bold and bold italic + if(doc.isBold(index)) { + var dleft, right; + dleft = dright = "**"; + if (doc.isItalic(index)) + { + // edbacher: changed this to handle bold italic properly. + dleft = "**_"; + dright = "_**"; + } + formatted = formatted.substring(0, index) + dleft + formatted.substring(index, lastIndex) + dright + formatted.substring(lastIndex); + + } + // Handle italic + else if(doc.isItalic(index)) { + formatted = formatted.substring(0, index) + '*' + formatted.substring(index, lastIndex) + '*' + formatted.substring(lastIndex); + } + + // Keep track of last position in text + lastIndex = index; + } + + var textElements = [formatted]; + return textElements; +} + + + +function handleListItem(item, state, depth) { + var textElements = []; + + // Prefix + var prefix = ''; + + // Add nesting level + for (var i=0; i 0) + prefix += ' '; + + // Push prefix + textElements.push(prefix); + + // Process childs + textElements = textElements.concat(processChilds(doc, state, depth)); + + // Add paragraph break + textElements.push('\n\n'); + break; + + case DocumentApp.ElementType.LIST_ITEM: + textElements = textElements.concat(handleListItem(doc, state, depth)); + textElements.push('\n'); + + if(state.nextDoc[depth-1].getType() != doc.getType()) { + textElements.push('\n'); + } + + break; + + case DocumentApp.ElementType.HORIZONTAL_RULE: + textElements.push('* * *\n'); + break; + + case DocumentApp.ElementType.FOOTNOTE: + textElements.push(' (NOTE: ' + doc.getText() + ')'); + break; + + case DocumentApp.ElementType.TABLE: + textElements = textElements.concat(processTable(doc)); + break; + + case DocumentApp.ElementType.TABLE_OF_CONTENTS: + textElements.push('[[TOC]]'); + break; + + case DocumentApp.ElementType.TEXT: + var text = handleText(doc, state); + textElements = textElements.concat(text); + break; + + case DocumentApp.ElementType.INLINE_IMAGE: + textElements = textElements.concat(handleImage(doc, state)); + break; + + case DocumentApp.ElementType.EQUATION: + var latexEquation = handleEquationFunction(doc, state); + latexEquation = "$$" + latexEquation.trim() + "$$"; + textElements.push(latexEquation); + break; + default: + throw("Unknown element type: " + doc.getType()); + } + + return textElements; +} + // Process each child element (not just paragraphs). function processParagraph(index, element, inSrc, imageCounter, listCounters) { // First, check for things that require no processing. @@ -375,6 +654,21 @@ function processParagraph(index, element, inSrc, imageCounter, listCounters) { return result; } + + + + + + + + + + + + + + + // Escape chars with a special meaning in Latex function latexSanitize(text) { text = text.replace("\\", "\\\\"); @@ -383,15 +677,15 @@ function latexSanitize(text) { } // Converte an Equation or Function element to a Latex expression -function handleEquationFunction(func) { - Logger.log("Equation converter handling: " + func.getType()); +function handleEquationFunction(func, state) { + //Logger.log("Equation converter handling: " + func.getType()); var equation = ""; for(var i=0; i Date: Fri, 9 May 2014 18:16:21 +0200 Subject: [PATCH 13/29] Removed previous implementation. Loss of features: Code sections are not supported right now. --- converttomarkdown.gapps | 402 +++++----------------------------------- 1 file changed, 42 insertions(+), 360 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 7455cdb..1bf8b46 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -64,6 +64,7 @@ function ConvertToMarkdownEmail() { { "attachments": convertedDoc.attachments }); } + // Convert current document to file and save it to GDrive function ConvertToMarkdownFile() { // Convert to markdwon @@ -156,85 +157,6 @@ function markdown() { }; } -// Covert current document to markdown -function ConvertToMarkdown() { - var numChildren = DocumentApp.getActiveDocument().getActiveSection().getNumChildren(); - var text = ""; - var inSrc = false; - var inClass = false; - var globalImageCounter = 0; - var globalListCounters = {}; - - // edbacher: added a variable for indent in src
 block. Let style sheet do margin.
-  var srcIndent = "";
-  
-  var attachments = [];
-  var files = []; 
-  
-  // Walk through all the child elements of the doc.
-  for (var i = 0; i < numChildren; i++) {
-    var child = DocumentApp.getActiveDocument().getActiveSection().getChild(i);
-    var result = processParagraph(i, child, inSrc, globalImageCounter, globalListCounters);
-    globalImageCounter += (result && result.images) ? result.images.length : 0;
-    if (result!==null) {
-      if (result.sourcePretty==="start" && !inSrc) {
-        inSrc=true;
-        text+="
\n";
-      } else if (result.sourcePretty==="end" && inSrc) {
-        inSrc=false;
-        text+="
\n\n"; - } else if (result.source==="start" && !inSrc) { - inSrc=true; - text+="
\n";
-      } else if (result.source==="end" && inSrc) {
-        inSrc=false;
-        text+="
\n\n"; - } else if (result.inClass==="start" && !inClass) { - inClass=true; - text+="
\n"; - } else if (result.inClass==="end" && inClass) { - inClass=false; - text+="
\n\n"; - } else if (inClass) { - text+=result.text+"\n\n"; - } else if (inSrc) { - text+=(srcIndent+escapeHTML(result.text)+"\n"); - } else if(result.text && result.text.length>0 && result.singleLb) { - text+=result.text+"\n"; - } else if (result.text && result.text.length>0) { - text+=result.text+"\n\n"; - } - - - if (result.images && result.images.length>0) { - for (var j=0; j/g, '>'); @@ -430,6 +352,45 @@ function handleImage(image, state) { return textElements; } + +// Escape chars with a special meaning in Latex +function latexSanitize(text) { + text = text.replace("\\", "\\\\"); + text = text.replace("%", "\\%"); + return text; +} + + +// Converte an Equation or Function element to a Latex expression +function handleEquationFunction(func, state) { + //Logger.log("Equation converter handling: " + func.getType()); + var equation = ""; + + for(var i=0; i'; - } else { - // Prefix for headlines and lists - prefix = findPrefix(index, inSrc, element, listCounters); - var singleLb = prefix.singleLb; - prefix = prefix.prefix; - - Logger.log("single lb: " + singleLb); - result.singleLb = singleLb; - - // Join all text elements and add formatting - var pOut = ""; - for (var i=0; i): - if (gt === DocumentApp.GlyphType.BULLET - || gt === DocumentApp.GlyphType.HOLLOW_BULLET - || gt === DocumentApp.GlyphType.SQUARE_BULLET) { - prefix += "* "; - } else { - // Ordered list (
    ): - var key = listItem.getListId() + '.' + listItem.getNestingLevel(); - var counter = listCounters[key] || 0; - counter++; - listCounters[key] = counter; - prefix += counter+". "; - } - - // Check if this item is followed by another list item - var num = DocumentApp.getActiveDocument().getActiveSection().getNumChildren(); - if(index + 1 < num) { - var next = DocumentApp.getActiveDocument().getActiveSection().getChild(index + 1); - if(next.getType() == DocumentApp.ElementType.LIST_ITEM) { - singleLineBreak = true; - } - } - - } - } - - return { - 'prefix' : prefix, - 'singleLb' : singleLineBreak - }; -} - -function processTextElement(inSrc, txt) { - if (typeof(txt) === 'string') { - return txt; - } - - var pOut = txt.getText(); - if (! txt.getTextAttributeIndices) { - return pOut; - } - - var attrs=txt.getTextAttributeIndices(); - var lastOff=pOut.length; - - for (var i=attrs.length-1; i>=0; i--) { - var off=attrs[i]; - var url=txt.getLinkUrl(off); - var font=txt.getFontFamily(off); - if (url) { // start of link - if (i>=1 && attrs[i-1]==off-1 && txt.getLinkUrl(attrs[i-1])===url) { - // detect links that are in multiple pieces because of errors on formatting: - i-=1; - off=attrs[i]; - url=txt.getLinkUrl(off); - } - pOut=pOut.substring(0, off)+'['+pOut.substring(off, lastOff)+']('+url+')'+pOut.substring(lastOff); - } else if (font) { - if (!inSrc && font===font.COURIER_NEW) { - while (i>=1 && txt.getFontFamily(attrs[i-1]) && txt.getFontFamily(attrs[i-1])===font.COURIER_NEW) { - // detect fonts that are in multiple pieces because of errors on formatting: - i-=1; - off=attrs[i]; - } - pOut=pOut.substring(0, off)+'`'+pOut.substring(off, lastOff)+'`'+pOut.substring(lastOff); - } - } - if (txt.isBold(off)) { - var d1 = d2 = "**"; - if (txt.isItalic(off)) { - // edbacher: changed this to handle bold italic properly. - d1 = "**_"; d2 = "_**"; - } - pOut=pOut.substring(0, off)+d1+pOut.substring(off, lastOff)+d2+pOut.substring(lastOff); - } else if (txt.isItalic(off)) { - pOut=pOut.substring(0, off)+'*'+pOut.substring(off, lastOff)+'*'+pOut.substring(lastOff); - } - lastOff=off; - } - return pOut; -} \ No newline at end of file From 4cc40e83c2ba1bfa4b328f5809d5846785fe08bc Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Sat, 10 May 2014 21:08:27 +0200 Subject: [PATCH 14/29] Added support for more element types. --- converttomarkdown.gapps | 175 ++++++++++++++++++++++++++-------------- 1 file changed, 116 insertions(+), 59 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 1bf8b46..f73e8b0 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -1,22 +1,10 @@ -/* -Usage: - Adding this script to your doc: - - Tools > Script Manager > New - - Select "Blank Project", then paste this code in and save. - Running the script: - - Tools > Script Manager - - Select "ConvertToMarkdown" function. - - Click Run button. - - Converted doc will be mailed to you. Subject will be "[MARKDOWN_MAKER]...". -*/ - // Open handler to add Menu function onOpen() { var ui = DocumentApp.getUi(); - ui.createMenu('Export') - .addItem('Markdown file', 'ConvertToMarkdownFile') - .addItem('Markdown email', 'ConvertToMarkdownEmail') - .addItem('Latex equation', 'ConvertEquation') + ui.createMenu('Markdown') + .addItem('Export File', 'ConvertToMarkdownFile') + .addItem('Export Email', 'ConvertToMarkdownEmail') + .addItem('Latex Equation', 'ConvertEquation') .addToUi(); DocumentApp.getActiveDocument().add @@ -89,13 +77,23 @@ function ConvertToMarkdownFile() { folder = parent.getFolders()[folder]; if(folder.getName() == 'target') { - Logger.log("Trashing target folder..."); - folder.setTrashed(true); - break; + var ui = DocumentApp.getUi(); + var result = ui.alert( + 'Existing target folder found!', + 'WARNING: delete all contents of target folder?', + ui.ButtonSet.YES_NO); + if(result == ui.Button.YES) { + Logger.log("Trashing target folder..."); + folder.setTrashed(true); + break; + } else { + Logger.log("Do not delete target folder, stopping!"); + return; + } } } - // Create new target folder if none exists + // Create new target folder Logger.log("Creating output folder..."); var found = parent.createFolder("target"); @@ -112,9 +110,7 @@ function ConvertToMarkdownFile() { found.createFile(DocumentApp.getActiveDocument().getName() + ".md", convertedDoc.text, "text/plain"); } -function markdown() { - var doc = DocumentApp.getActiveDocument().getActiveSection(); - +function processSection(section) { var state = { 'inSource' : false, 'images' : [], @@ -122,7 +118,45 @@ function markdown() { 'prevDoc' : [], 'nextDoc' : [], }; - var textElements = processDocument(doc, state, 0); + var textElements = processElement(section, state, 0); + return { + 'textElements' : textElements, + 'state' : state, + }; +} + + +function markdown() { + // Text elements + var textElements = []; + + // Process header + var head = DocumentApp.getActiveDocument().getHeader(); + if(head != null) { + var teHead = processSection(head); + textElements = textElements.concat(teHead.textElements); + textElements.push('\n\n'); + textElements.push('---'); + textElements.push('\n\n'); + } + + // Process body + var doc = DocumentApp.getActiveDocument().getBody(); + doc = processSection(doc); + textElements = textElements.concat(doc.textElements); + + // Process footer + var foot = DocumentApp.getActiveDocument().getFooter(); + Logger.log("foot: " + foot); + if(foot != null) { + var teFoot = processSection(foot); + textElements.push('\n\n'); + textElements.push('---'); + textElements.push('\n\n'); + textElements = textElements.concat(teFoot.textElements); + } + + // Build final output string var text = textElements.join(''); // Replace critical chars @@ -130,13 +164,13 @@ function markdown() { // Debug logging Logger.log("Result: " + text); - Logger.log("Images: " + state.imageCounter); + Logger.log("Images: " + doc.state.imageCounter); // Build attachment and file lists var attachments = []; var files = []; - for(var i in state.images) { - var image = state.images[i]; + for(var i in doc.state.images) { + var image = doc.state.images[i]; attachments.push( { "fileName": image.name, "mimeType": image.type, @@ -162,14 +196,14 @@ function escapeHTML(text) { return text.replace(//g, '>'); } -function processTable(element) { +// Add repeat function to strings +String.prototype.repeat = function( num ) { + return new Array( num + 1 ).join( this ); +} + +function handleTable(element, state, depth) { var textElements = []; - String.prototype.repeat = function( num ) - { - return new Array( num + 1 ).join( this ); - } - textElements.push("\n"); function buildTable(size) { @@ -190,7 +224,10 @@ function processTable(element) { // Add table data for(var ic=0; ic Date: Sat, 10 May 2014 21:15:27 +0200 Subject: [PATCH 15/29] Update README.md --- README.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ccd7217..26c44f6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen * File -> Save * Running the script (run as many times as you want): - - Export > Markdown email + - Markdown > Export File (Creates a new folder `target` in the same directory as the document. Markdown and image files are stored in it. A warning is generated if a `target` directory already exists) + - Markdown > Export Email (Sends you an email with the Markdown and image files) + - Markdown > Latex Equation (Put your cursor on a equation and run the script. It will output the LaTex formatting in a dialog) ## Interpreted formats @@ -27,19 +29,16 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen * bullet lists are converted to "`*`" Markdown format appropriately, including nested lists * Images: * images are correctly extracted and sent as attachments + * Drawings: + * not supported * Equations: - * Equations are converted to LaTex equation strings and surrounded by ``$`` signs - * Blocks: - * Table of contents is replaced by `[[TOC]]` - * blocks of text delimited by "--- class whateverclassnameyouwant" and "---" are converted to `
    ` - * Source code: - * **UPDATED**: blocks of text delimited by "--- source code" or "--- src" and "---" are converted to `
    `
    -      * **NEW**: blocks of text delimited by "--- source pretty" or "--- srcp" and "---" are converted to `
    `
    -    * Tables:
    -      * **NEW**: Simple `` processing
    -  * "--- jsperf ``" is replaced by an iframe that shows an interactive chart of a JSPerf test. The `` is the last part of the URL of the Browserscope anchor in your JSPerf test. Something like `"agt1YS1wcm9maWxlcnINCxIEVGVzdBjlm_EQDA"` in the URL `http://www.browserscope.org/user/tests/table/agt1YS1wcm9maWxlcnINCxIEVGVzdBjlm_EQDA`
    - 
    -
    +    * Equations are converted to LaTex equations and surrounded by ``$`` signs 
    +  * Table of contents:
    +    * Is replaced by `[[TOC]]`
    +  * Line breaks: 
    +    * Inserts a `---`
    +  * Tables
    +    * Converted to Markdown tables following GitHub Markdown syntax. 
     
     ## CONTRIBUTORS
     
    
    From 6bc4f96aad29296465c2f075de8b0b5e2dcca995 Mon Sep 17 00:00:00 2001
    From: Andreas Wolke 
    Date: Sat, 10 May 2014 21:28:31 +0200
    Subject: [PATCH 16/29] Source code support
    
    ---
     converttomarkdown.gapps | 19 ++++++++++++++++++-
     1 file changed, 18 insertions(+), 1 deletion(-)
    
    diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps
    index f73e8b0..0cc8ddf 100644
    --- a/converttomarkdown.gapps
    +++ b/converttomarkdown.gapps
    @@ -117,6 +117,7 @@ function processSection(section) {
         'imageCounter' : 0,
         'prevDoc' : [],
         'nextDoc' : [],
    +    'inSource' : false,
       };
       var textElements = processElement(section, state, 0);
       return {
    @@ -479,8 +480,14 @@ function processElement(element, state, depth) {
           textElements = textElements.concat(processChilds(element, state, depth));
           
           // Add paragraph break only if its not the last element on this layer
    -      if(state.nextDoc[depth-1] != element)
    +      if(state.nextDoc[depth-1] == element)
    +        break; 
    +      
    +      if(state.inSource)
    +        textElements.push('\n');
    +      else
             textElements.push('\n\n');
    +      
           break; 
           
         case DocumentApp.ElementType.LIST_ITEM:
    @@ -525,6 +532,16 @@ function processElement(element, state, depth) {
           
         case DocumentApp.ElementType.TEXT:
           var text = handleText(element, state);
    +      
    +      // Check for source code delimiter
    +      if(/^```.+$/.test(text.join(''))) {
    +        state.inSource = true; 
    +      }
    +      
    +      if(text.join('') === '```') {
    +        state.inSource = false; 
    +      }
    +      
           textElements = textElements.concat(text);
           break;
     
    
    From 3fa51cf161cbdb51732696436a92fa916d30692e Mon Sep 17 00:00:00 2001
    From: Andreas Wolke 
    Date: Sat, 10 May 2014 21:29:54 +0200
    Subject: [PATCH 17/29] Update README.md
    
    ---
     README.md | 4 +++-
     1 file changed, 3 insertions(+), 1 deletion(-)
    
    diff --git a/README.md b/README.md
    index 26c44f6..920da32 100644
    --- a/README.md
    +++ b/README.md
    @@ -37,8 +37,10 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen
         * Is replaced by `[[TOC]]`
       * Line breaks: 
         * Inserts a `---`
    -  * Tables
    +  * Tables:
         * Converted to Markdown tables following GitHub Markdown syntax. 
    +  * Source code: 
    +    * Source code sections are started by ````string` and ended by `````. Single line breaks between paragraphs are used within source code sections.  
     
     ## CONTRIBUTORS
     
    
    From 31865db8404a1645bf45b42a87c2b9f1255448fc Mon Sep 17 00:00:00 2001
    From: Andreas Wolke 
    Date: Sat, 10 May 2014 21:32:26 +0200
    Subject: [PATCH 18/29] Update README.md
    
    ---
     README.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/README.md b/README.md
    index 920da32..2928ff5 100644
    --- a/README.md
    +++ b/README.md
    @@ -40,7 +40,7 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen
       * Tables:
         * Converted to Markdown tables following GitHub Markdown syntax. 
       * Source code: 
    -    * Source code sections are started by ````string` and ended by `````. Single line breaks between paragraphs are used within source code sections.  
    +    * Source code sections are started by ```string and ended by ```. Single line breaks between paragraphs are used within source code sections.  
     
     ## CONTRIBUTORS
     
    
    From 1605fbba90cd560282fa36d3e9d781048b88ab3d Mon Sep 17 00:00:00 2001
    From: Andreas Wolke 
    Date: Sat, 10 May 2014 21:35:20 +0200
    Subject: [PATCH 19/29] Update README.md
    
    ---
     README.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/README.md b/README.md
    index 2928ff5..36bab92 100644
    --- a/README.md
    +++ b/README.md
    @@ -40,7 +40,7 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen
       * Tables:
         * Converted to Markdown tables following GitHub Markdown syntax. 
       * Source code: 
    -    * Source code sections are started by ```string and ended by ```. Single line breaks between paragraphs are used within source code sections.  
    +    * Fenced code blocks are started by three back-ticks and a string and ended by three back-ticks. Single line break is used within fenced code blocks. 
     
     ## CONTRIBUTORS
     
    
    From 1b163967cc25c0240502fd522317ae6b51e25511 Mon Sep 17 00:00:00 2001
    From: Andreas Wolke 
    Date: Sat, 10 May 2014 22:05:47 +0200
    Subject: [PATCH 20/29] Fixed invalid method name
    
    ---
     converttomarkdown.gapps | 21 +++++++++++++++------
     1 file changed, 15 insertions(+), 6 deletions(-)
    
    diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps
    index 0cc8ddf..ba9150f 100644
    --- a/converttomarkdown.gapps
    +++ b/converttomarkdown.gapps
    @@ -1,13 +1,22 @@
     // Open handler to add Menu
    -function onOpen() {
    +function onOpen(e) {
       var ui = DocumentApp.getUi();
    -  ui.createMenu('Markdown')
    +  
    +  if (e && e.authMode == ScriptApp.AuthMode.NONE) {
    +    ui.createMenu('Markdown')
    +      .addItem('Latex Equation', 'ConvertEquation')
    +      .addToUi();
    +  } else {
    +    ui.createMenu('Markdown')
           .addItem('Export File', 'ConvertToMarkdownFile')
           .addItem('Export Email', 'ConvertToMarkdownEmail')
           .addItem('Latex Equation', 'ConvertEquation')
           .addToUi();
    -  
    -  DocumentApp.getActiveDocument().add
    +  }
    +}
    +
    +function onInstall(e) {
    +  onOpen(e);
     }
     
     function ConvertEquation() {
    @@ -37,7 +46,7 @@ function ConvertEquation() {
     // Convert current document to markdown and email it 
     function ConvertToMarkdownEmail() {
       // Convert to markdown
    -  var convertedDoc = ConvertToMarkdown(); 
    +  var convertedDoc = markdown(); 
       
       // Add markdown document to attachments
       convertedDoc.attachments.push({"fileName":DocumentApp.getActiveDocument().getName()+".md", 
    @@ -80,7 +89,7 @@ function ConvertToMarkdownFile() {
           var ui = DocumentApp.getUi(); 
           var result = ui.alert(
             'Existing target folder found!',
    -        'WARNING: delete all contents of target folder?',
    +        'Delete all contents of target folder?',
             ui.ButtonSet.YES_NO);
           if(result == ui.Button.YES) {
             Logger.log("Trashing target folder..."); 
    
    From 2ea56ef56ee7fb3df5bfd48c9c2efaf7fd945ab8 Mon Sep 17 00:00:00 2001
    From: Andreas Wolke 
    Date: Sat, 10 May 2014 22:28:08 +0200
    Subject: [PATCH 21/29] Fixed error in prev and next pointers.
    
    ---
     converttomarkdown.gapps | 29 ++++++++++++++++++++++++-----
     1 file changed, 24 insertions(+), 5 deletions(-)
    
    diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps
    index ba9150f..9670f69 100644
    --- a/converttomarkdown.gapps
    +++ b/converttomarkdown.gapps
    @@ -127,6 +127,7 @@ function processSection(section) {
         'prevDoc' : [],
         'nextDoc' : [],
         'inSource' : false,
    +    'size' : [],
       };
       var textElements = processElement(section, state, 0);
       return {
    @@ -439,10 +440,24 @@ function handleEquationFunction(func, state) {
     
     
     function processChilds(doc, state, depth) {
    +  // Text element buffer
       var textElements = []
    +  
    +  // Keep track of child count on this depth
    +  state.size[depth] = doc.getNumChildren(); 
    +  
    +  // Iterates over all childs
       for(var i=0; i < doc.getNumChildren(); i++)  {
         var child = doc.getChild(i); 
    -    state.nextDoc[depth] = (i+1 < doc.getNumChildren())?doc.getChild(i+1) : child;
    +    
    +    // Update pointer on next document
    +    var nextDoc = (i+1 < doc.getNumChildren())?doc.getChild(i+1) : child;
    +    state.nextDoc[depth] = nextDoc; 
    +    
    +    // Update pointer on prev element 
    +    var prevDoc = (i-1 >= 0)?doc.getChild(i-1) : child;
    +    state.prevDoc[depth] = prevDoc; 
    +    
         textElements = textElements.concat(processElement(child, state, depth+1)); 
       }
       return textElements;
    @@ -450,9 +465,6 @@ function processChilds(doc, state, depth) {
     
     
     function processElement(element, state, depth) {
    -  var prevDoc = state.prevDoc[depth] || element; 
    -  state.prevDoc[depth] = element; 
    -  
       // Result
       var textElements = [];
         
    @@ -564,7 +576,14 @@ function processElement(element, state, depth) {
           
         case DocumentApp.ElementType.EQUATION: 
           var latexEquation = handleEquationFunction(element, state); 
    -      latexEquation = "$" + latexEquation.trim() + "$"; 
    +
    +      // If equation is the only one in a paragraph - center it 
    +      var wrap = '$'
    +      if(state.size[depth-1] == 1) {
    +        wrap = '$$'
    +      }
    +      
    +      latexEquation = wrap + latexEquation.trim() + wrap; 
           textElements.push(latexEquation);
           break; 
         default:
    
    From dadf379118c05e4c3798acf0163286cb42e9f109 Mon Sep 17 00:00:00 2001
    From: Andreas Wolke 
    Date: Sat, 10 May 2014 23:09:20 +0200
    Subject: [PATCH 22/29] Catch more error conditions.
    
    Concerns documents with multiple users.
    ---
     converttomarkdown.gapps | 21 ++++++++++++++++-----
     1 file changed, 16 insertions(+), 5 deletions(-)
    
    diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps
    index 9670f69..077399c 100644
    --- a/converttomarkdown.gapps
    +++ b/converttomarkdown.gapps
    @@ -51,10 +51,16 @@ function ConvertToMarkdownEmail() {
       // Add markdown document to attachments
       convertedDoc.attachments.push({"fileName":DocumentApp.getActiveDocument().getName()+".md", 
                                      "mimeType": "text/plain", "content": convertedDoc.text});
    -  
    +
    +  // In some cases user email is not accessible  
    +  var mail = Session.getActiveUser().getEmail(); 
    +  if(mail === '') {
    +    DocumentApp.getUi().alert("Could not read your email address"); 
    +    return;
    +  }
       
       // Send email with markdown document
    -  MailApp.sendEmail(Session.getActiveUser().getEmail(),
    +  MailApp.sendEmail(mail,
     					"[MARKDOWN_MAKER] "+DocumentApp.getActiveDocument().getName(),
     					"Your converted markdown document is attached (converted from "+DocumentApp.getActiveDocument().getUrl()+")"+
     					"\n\nDon't know how to use the format options? See http://github.com/mangini/gdocs2md\n",
    @@ -72,10 +78,15 @@ function ConvertToMarkdownFile() {
       var file = DocsList.getFileById(id);
       var parents = file.getParents();
       
    -  if(parents.length > 1)
    -  {
    +  if(parents.length > 1) {
         Logger.log("File has multiple parent directory. Script does not work in this case"); 
    -    return null; 
    +    DocumentApp.getUi().alert("Document must not be in multiple directories");
    +    return; 
    +  }
    +  
    +  if(parents.length == 0) {
    +    DocumentApp.getUi().alert("Document has to be in a directory for the export"); 
    +    return; 
       }
       
       // Use first parent
    
    From dd925874024f2d2ebc981252bc2598883f95079d Mon Sep 17 00:00:00 2001
    From: Andreas Wolke 
    Date: Sat, 10 May 2014 23:25:26 +0200
    Subject: [PATCH 23/29] Added screenshot
    
    ---
     README.md    |   2 ++
     markdown.png | Bin 0 -> 27988 bytes
     2 files changed, 2 insertions(+)
     create mode 100644 markdown.png
    
    diff --git a/README.md b/README.md
    index 36bab92..84d11ef 100644
    --- a/README.md
    +++ b/README.md
    @@ -3,6 +3,8 @@ gdocs2md
     
     A simple Google Apps script to convert a properly formatted Google Drive Document to the markdown (.md) format. 
     
    +![Screenshot Google Docs with gdocs2md](markdown.png)
    +
     ## Usage
     
       * Adding this script to your doc (once per doc):
    diff --git a/markdown.png b/markdown.png
    new file mode 100644
    index 0000000000000000000000000000000000000000..1f7ed22ed07797b77288ab0cad7c3411280b83ce
    GIT binary patch
    literal 27988
    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 `LEncCnTK0_8uicgX>1 zLTEbLxzZKBT9GgzYbO>*my8^3h8v^;6XA$^aWg)d9A2XCrCRHKni9h6m7~w``Xg~o zw%6jUMHbt>EEK{Xr(xSST))i+;$AbR0ZK#6De1KooPcqXOFfy|(dH;7Bw74#IBSA& z9zh;N5wmA&=nCm@hL7k&^#PRzOst8TyOm5PD=2T$Dg2?2+8o-dTD= zI~JlsJ86YrFcb05p(`d;5VrBiP`Yk%tt)=yHV*yxk!99CH$M9nlIb#MsXF1yb5hD2+Dv86J9iX&}=V_U2?NsN$))uZYbbg{W z-PE0W(1?}6kCkz|H`!!%$zy9++2Gf;wW~`q$^n>=cznn|&$oa^aSU$1cGgJ_*-mtq zl>k}SNuBzIVN%i+hPR(*9cK>6ej8F9l~<0?-hec?CJhN#j}e+sPEDXU*N07rx6SXy zce2{>e(R-aoR$7t7?JOgzBUKI*qbtq5a}g7*kADITS6?4QL_y{ejSaHSA7 ztre!bS;N4utBdJN#f&tOqxznC{9EnT3G%=ud|4ss00TJ0!QY0x)U zu^x3d-d&-vY^Bs4N?R3(GgQClVvcu@GH|E8kQdl-0`Jxo>ZT)c+9_j8pr6BNzoYuO zupba(2?TvYyDhOmm#5+2*k#%`ERN)!x~j}Ox zDzc<2?`&ZyXYp~Qm}XJ8j5k@yTj$0UmyZ;46G^j`zGfsUz632j4@RC!?(q04dVj?x z{0TErU+`L4@mohuSDir}h@&>A3bJ0hPVG~hTSFaCnb$6yAak~{+Z~BZ*(pvRfVu93 z_LTrIT2PAcPrRD>yb3XU{pW_DI;6Y1|ziN3?#P@i}8&~Mv zzwRSeki=hr#W=(zs(Tj0)k^mhjlFt3@Y<3OveahQgqk4ql{?fnft`#(17-q|1zb2x zDBnarTJR?NK&=`K7Dc6xs}q2x5(7mG!JF^(D4LfBm@N3eXl_!L3{%$D&0 zN_j`1fm+yb2kWEqxs)w`7rO)K)?IgXPkG(o7g)j=X(xu0t$obK0sGSGxLeJG>173k zKS;j(=7EDkK@%Pd>2|R1&I@ta#PJc&ox>sCjT66ar`M&%5 zWSICCxt%Ff_#gWs>ESva9XpT@sY6!==?;ZY9`?CsA&m&OsScr*9ySPtQwp{GHVnwN z1{O;^G|gdC46Y^0CuY~>{)-&(NhJJxS+0;&EhydAgfmmmP}+V^#uT|=pr2;2foC__ zQVdp-OS+30gXM}WZk@xSx%~%A_L^g5RGoEqby|Hz{HAzv;urQm1VB^|sY@SBLmS=@ggUw&FqKXL{k4#F8wY7;iKn$?}lQAUTr{Xi590 zc@Ep!4qFw%5|59mJ((q0-fitS`{<_XLPa7AsmX#YbjlZ%Xep^kHfPUZk-l40tJ=?t zA$8JBee;`TY${Jjih4k)rua&8XO5o|_c_w3Xx41|4&=y&m9>V}9A=^Jn*a*z6uKSJ z=Pgp_vnC!`mT1wiJF@hhZvq!$Cxej?8Tv#h5a%9e@pZkAPU7;gAr7ghLlctpY>YHoUJ zIE5=)$Ks*eA`C5M?!~kBsl`yU|0(1%UEkK3gmUISP^UQ^iQw3@y`j4PmvC_-HL=L2 zKE7dDDttz{8ayBP&@a>XcFGLoN!C!IeBF`E%RJC7LH6{PdnDTPP2GcTaKl#y&(!qE z8Xj$fSEEL^DOs}qbNtyHy|RCz3+ca2%$KFAO5K7)Q4{g?P zU)o&Q`8+~hJ7y~)c?Uf&t(@EzjhN0PCJHXD@ri*AoB4PQH>e?AiIXb$tcyQk{2EyfKeb@7FbPkrBs z%q-g^VrBXBD`s%x5`@IeV4=$sHCP4TkwB7yV@^Q1%CGHAMukYfbTid3r_WZ;A#t0( z#wT;;mTeo?RtuNt{z4BXoE;#9amsUWLR!?5C&jVY^mB3H)RKPD;`>HswrKgRHu?u? zRRAX%YQuPd^lcJKWru~Wi9)R0OYY{Y783>A1st-`p&t7RtiJ4EmH$>B@05+89eHKz zE=7(Rjuf9N+SE%S>fs|_=+8k^hbgg;tprjM1edQz92jHO>P_*@3f}i%YD*PcgizBR zQ}bo~W!692?p%FOkC?k1m$1jSs_iI!-Xk~6J={J60mr;Ilg&~K0FM_4+vwZ)PRlB|1?^A+kywv-X1cU@e?EfJ>RKosx z>pAZG3?ji!S{YHvzgQ34|A@%bqPV!*2&5DdAq{-F!L@X0j)#DPogMbnuq=MZQ`S2K z33wfm9UL6m#dKls``=lV?-74-C0l(8e`7809a$)UwNcGMbr01aXI{R;A>eniWb_I8 zKZN3}Jha8o)z~-Q%WgQ*f8?edg)kaj@vZ?J4tc!j#?wNQluy z7`s3J!6`vP=?Jdt<(9D3{ix@uIOGKUz8f;{u=>M|9?}5wdNsFiF7W0U+o;no`d}h= zr#lqR0H~GrZ~-QQ;C@%F#sBG6{8&QOzlt7EKyjn;_$nWoKqllaT@D-xozWtp;x;x)E*ws~{`0aH7|2^j2a^2hX_ja%M{A8|U zLUZADc-(Z>p%tpTSqpQF| zNP|`y?e3P_-DQ^8zouAQ`oOWO@4?fp1Xr}1ac_nZeIq~%L(~c$s0#U&R`-q!f9*>l zji=0Gfb<9k)bnOqSy>_Cu)E9*4-IXB4g~maj1AQ(hC$j?_`~Vd3VVOn+9H*61Y9aK zncE$KUGtTD((_l(7h4jJD=Pn6k%uwf4apQ5B_pKwc<+hP7Xq`si0u>a$T8&w%KOCn zgNrpr7>KILw2zDfC2Fk(X+)_5%A9Pmas?J?*2*C6P-YQC8BuI#N$@BR@@G3cT&tDu zKUfu2xui>ri}M{;n#37|YYh#6`?1t^MIl4jt<%^7RRBBBDP2vL^!*n-&S6e{y1{K4 zh6#JUv=nPiI#kvX^hspMx?NqK73q9&{Shm8>^0-}!!?;Nu@y2XzSqK{1k~P(Pu|hH zB&4Q|D#nyl{>89fUHyDlibbV1xJUky*cKGw|&xmtjs#hlhef{DLW?k)EIs z|F1W^<7PnyK!)07Ro}6XYyr>SVh^gGu`w|W*NF7>sI>USg}-Z6OW@|-6Ee@Q(Ma!` z+?_-kPMOC7sVN51C4Oh$Vv35W8q43C{L1*j&j-1IV0*&dA$(rPY_!yA*#>wYAXQtH zpD~G1@fKO~A=nv1P8zs(ynV-HsXUNCY`2%HV;GllI!k`O@U?c84h(n)aigqPRATim za`++3DSy3tS4G;li0fbeUv909OJtjuZ5^D|t@_6ys9X7J-&uCOXXxq)tZ) zytU_>e;mI0ygYi46O&$f#4dcnQ3n}Ee$Z+_DPN_tfY9Fad+(K;oSS1tCi8U_@I1v^ zJlO7crRscpeX40{!T~wYt}U)KnVFeg?0gXkG3qw*=F>t&LPA0&0W?oacMH4)K|pB; z?8V`8it_iQwc5TXn)63{g1)x2*m4o_Y01pbrwkb4J!m9vlyxRSeJR_76u zt%UJe^`CkLnBeyv7SBpP^Lo?*J$qmZJKG~*&jX`vy1q<4+d^6`-xqd+`RpH?`G-Bj zY&(BElrfs_013|hJ@AZg7tkFyt4~TCPw`kDyfAC%*^m)%&qIq(oxt-?Y+l#DvcCvu z;M>=+>&vlM919l?HmUv?c3Y=rA{EA#0nsCh$tXrA3>XYC=lT3>$GS((3g3C4u9$Kd z=jCH#!Z&yLVj_Pk9Or|Nuu?0-_OQ8tH&<0oY0$xTSJDeU z^#sczvVDi93H!PvD+Yyu+KGJPjBzYYHp+X z>+?gi>t4(zwOWa~X0!M-aYZuVp!Bg@2>CF2CaP<(F%fXEu?YR78#!h9m{B9fmwxIE zd2(jzT=^5zGa>R4C&`xZG|7*o4qsZ3#$;xNu!bs-9`MI!GiYPwe#n=G;t|#?(*NgY za-U-iwj=Tvt_Urn#LwCjG*7I zTfCuV*UwVBS#uq>GbQ2CkR_jTKl+qL_0W{b=UEQbbF@p%fs2!@t(53X0B@HAq#pBE zql4LXgjd6hD=SESX^kXN^4u$5glcQvEOXlzXO;#?1DWW?Ppy8XVm1|AAJ0`}^0`n* zClHmT_K9hszr8nM)LKuF;1LiY9&;e^J%K!&OT0&Njh~J2Sra+bXSY+M3pSA9=9kj= z6BdIR1vphyE>NZ9k-Vaxm1FIrrD_Q;&vBf{IC~&4y&8VkHH$u<%(?I_S4n}c7A_{P z`onU5)Bt!sygfdNNYX4&Y*=zuVy>b)br3Q}13tx*#yhYqY}U7aK=Nc24HPiyJJG%@ z`%_uTMDCEl!N*X(ag|ap_l_k_Xf1j3`0@7G&#-Hk1Du{Scmh5dd|~w>8W^)LH%}qK z1N0&}7H&vi85f#}c);l?#k$B+h zYfF)z0ioPUAE=+#He|j%Me8)WBPp+%gxX+U;~h!m28OQtv;3C@h_b@%3Tqs&y*Bj1 zV|?OH)#K%GnKXsu*v}BdKJ`0z09X{}dof+G+S9YS=EDThMw@Iy`al@ofwBSHU&1Y+hzVz@sxX}{Uu(o27 zuj|pj!fXYnJ}hlpPOUq_TVpN4cp$oP0)W}ZNHRzov-)YdHq>M$Fggp zZ^MZ>8}Np1d+jsNfa|a~gR=Bacn@9dFn82WnM1?x)>z&2g!%F$GmDLDI=nHE$G*YbN-V58C8@_eCLQ3{_ZM(V@NOyN5-9vZRNXO85kN*Dm-VgV~ z{WQB(c6{X}$ol2o#Yn7fmKNJ}rsIBm+@ywd~y{`-vi=opp$J{^Ow2b-Ulb$EhNbm>7C z9_8h0JATO72mEO-7?wIiw4KL37Xt$Q=pv>E*f_Qp+My7GBnqPC&paM0{C$yt!>wCR zEE`lQbvHr#Qv}IfewRbU@ru#VUL#>K&dbqeqxXy{K#}o|X4H;%T=+Qt>b+ z`9jP?b}AN$9OXC88n=?hHlKtCq88C{O({Q7+GakFz4iJ)F(b&_+RB0{E4mkO17i5e z8*i%W!E_{1LHni5DOhUV7?0^yW9eQrQJJUkTTJJ$W~u8V-@_V<5MEi4(k&U|@bsU9 zgvMRZTruZ@o);@BeALxg_>C+a5dQjO@G&OjHz%JKY>{8{RW|umTb>lJ)kTJ@#e}{}B|OapsYexE@{TrJGdgl`HdbA%{@2F!kJ<$oDK-H+?E_ ziME$jQ0(=D+uLliXUq?maqA_=9CDuyx*26o#UI{CdxoW%x?#SRablq7r1+fw5(N%p zG`6s%__3kLh&5Mf@qL+1-SoRgj6?zUoCZnQ=li%pylgxKn>@^BEc=NDyz`wr>4p}6 zE~yf{ljVgoP4?@ChKBgMZPeM`+(pa%^;@~b*B>kb?GF~4E~Z-UUJ&e;lPF34N+8#E zR@n@dqs&sNG|esO$uAfVMYf2o*Ga?fs5L^s4Meaz8JQS~;{j_*IB;t7s}B0w8|BP( z7}NV;@2Jkjky-^fGEK~POyh}Bi6=$ylhBXGq>Tsl#;Ny^#QNf&F$2d2VYTJ+Gkwe1 zlpBI{`YVwFB1qhIMTge!r^3IQ7@wlNoc(0wj0Z#@&XLubpG~`rCOZsoEUHopiQl$| z_Bia44@N~#NW?GL^gOXctGcL?L2y?zyB9TKF2 zdR}W;#vaj5K^HF(Y*{(Dv6W$i)0mN0LYnUQvybx89?vpV@XVRz>Rh2!CB4 z*(DP^@8BQ5VoYYSA^b$WC7l^AeOdC_ikGikH2FO6cRkZ!qUS;Ai0muwFYcJ8Z{yh! zR%?fyNjW54vC)=AGTL&QDzRpPa5E8Gn#fXSq9lJ_kaZcOE#cEbFLE>F_ZNjTRN^n+ zNLGYLP?!)$nmXi`2%lh#zG?Gnv&Wkye}XXODXj_ZXrMm(Z3b~z*L_UCGebEo(a|cz zg~m_3I$|n=HYwN`Ry{lQQ0EZ_5hxqnAB}vEKoyAn6<4fGB2Ut&ACnwdnsGDDH%&7K$B?ZYe zE{j_?2-%aR9!~R6NN%5CIpL&FKgB`+#)?RNJ=S(BjU_kz?kv3h-39Ag^op)xiT#)5 z&_>3&uExl(#l-V_`u;TP zRdw>9hJ1VF@%xFrshLm*!<~Cvxm)AX)!S@W`+Ls)h0t#0REkP{lCVJh6dtIQr(6_v zu%3GYmQcCl{PbshmO8AAyc5G8!Zy^5k_JRSkXOYc*bJtFhc_nQX=82Ca!SOE<}RPC z-=t!7y^M`PwuFFj!@wCvp61qNwUK8R=m+25*NsXyjF#tw^u<_LMPeFZS~oe`ikGX zF$Z2l%1;3zK(Pt`E}^ui#>}hb*EQ3Ylj(jtgsNGhjUMvikQ4A!i9ySB1IC^$hzRKJx;kVB7R-!R?EV5pl7>U z0hd^*YrM)$^S#wPYBF%Km%XxF=#n4dI+rx9U5c~I6A|>vogv^7!ADEJPmb7~C?(zD zEE|G&VrP;52z<}wGUy}yz2mMcx>O^yyaiCwL-H82cSR(GurC-6)*1{B(?S8Yr&1LL z{(%xH6wO3QX4L3;Nj%s6svaaHPtS|VQ%6pT5PV}?IqdPv0Q8xJ&q z0(@=6X5Fax9coPLA+<%nC9AR9IEj(*l@0MatHm4a;kEk!vB!J#1<8o1qPvt+dv#l& zmBo>(_IWOs1CEpmRVck!WjQg3_1SBuTr<)A(Nl42;>v6j9N_-;eRz`~V~_+B8oX4{ zCJIxz#*pIK$qdupjxwB8x6s zzNGwa!)dBc8cd(x=8!n^A3*G&G3S%*JdgbOgIa?~5N7LDq@z=OuGA_X7LTj^;K;9M z4}sLlb3A{AL$n#$ISaZ6>QbzA3tjA$zU80*;~3p)6u>D&9eCykoJP1o+E0#Yo|qMh zXzo&mf?#_`Y5j7JB9s9=4MB(6!z4*@sX-$`jH3Yitdxz1FNK*SDps&UO;~nFJFkeB zUjKo-am4R8zM0$au*Lz6mdf_@v7igaQ93bu41MPUJL6oswx;^kfNMESgXbcNWO$Np zl5^@Df9-|RSaq}8VKk=GQKBE%{KOU&0FMfaI|j4KInt=&|F zB(I9aty@~bD4StIV~`m9-G^t29v{MlLVvbcfAU!oLq${EXMO`$ocLK+8~P6E2ioh= zYz8Z`v&9-0y+A|}tq2+sQ$>!8^%1tZ8=wtPFvYPWBGdg|=76J^Hw4pv9$QC|xP6;f z=yS;WZgvb#f~P9({#Tz?c*;vfI|!Q#YHLSq*ZS%|yPM{l?|i zSdJ6_Tu>p=KnR_R0co@u*-NDaHD}8-&NyR%&^LT06_!7OzDvJikIZ;JCwX+MMa9J* z`y_ERTESwBMS^RSbq}=~I%T4PUlFEFTOcbj>vDhRS!uhZAAypY)w=L|V)?-RT_~CS zo~*trgTGiA;)MW7?Dyq<{t+RiV{Z$ZOab!dkdRw05nGYOM`Wu?u3ibXWoi?CeG(fF zt-^J6J1;);haP=PmZGvP^IW};{rNDx(KUamB(U37`<$)8Q<-UFr;%vG1Qd1$k7eLz1Uqir2^4_k4(MH?J(tA$r#6Sl506@5hUhZhC%*cg`TskY~eHkrG?-x{g3aA|L0n+rl5 zDmaFqO)F&lFks=Awx+IRqLpJ1T*zAG_^8Jm+QrH z8(fGokr2|F@3fboq3#gs@#1`(X|j+5tjOEW*DA$;d4cqV1+UR_!0rJdwrRAUQ*P6& zIW5E+tGF}u8|NfUvz0?t3yuqIDl*|_w#Q)n{2`aZ+_|UC-YH_h8gZwpliUnxVAm{m zP&!XyG?I)%`$r{oyz9}i&=eT6v8gFgckE?YNJ&YFHtG8F495!reoLn!%Y_7h(l`Xl zB~a2_DhXh0Gk(aKW=!XE*7N?)bz2$r0G<)^W;(Rb7Rs;_E zx2hhE{FJv{3(ZJ$qj?KWz_tsF1R+Y+UbJ0TnWkUOvLOsd3(x znA$uqVTw$qk5eEJL@#KXdIB&3utOtVi0GVTkv_L(=z0)cBGbnN?2tYOHIunq>*kC< zcYTvz%4jBci$bl9G4jYzU=3zqhcAuqS9*Zf`nr2vf6!i**F?BkHYT@AGQ)3Kh1B9L zJp{Oxd>X*vUV7zFR;r8{orD=AP=$cKBOah@4=is2`siCfNN%y(TmRU4$5(+Dm14Tu z)>~m}w=T7Uof`(xqepz@9!<~VghTz!=5j0F_^LU6m3U4`x^VBbcmPyNa|KGL!Kx#) zT^EkMiYWUd(yggKzbkB-;U72Q_VA8`@G$^%hWqv6P7G9R>l#!6Z5$H!=UN;K?=H=^ zSFL|A^&TT@|FjF|rz!T&di&g-JHTJ2;7{Opi>Lu>YM@3_A@k$kiv5JSN!as^FcoRF z^Lkf!Y*WkQK8FQ7L0czzNyu|bzPiW>wKtLSP7Eb?Fxc*R_s8c$y8sP&A8~Wz!L7)s z0vCi$;x*r}(x99CWkcBzC1FUd{}?`ZY%Yzge+*xxGT-`VfZ-zwd93Q+U5xRIZCWB4 zneczPC0`*~_}JK3-EpCe;)==t)Z+Z5P5A8vg3F*;+{T7;(RJ-fF<8S&^`du&5hEO2 zK}l~qXFmMP*N(P4LQBK|836>8gClGXAg_I=9-(i7){`*NVH=kvEq56GoqbU1#$do> zWB?Bd6psvi=>L-Z6$?Tw?Co3X|KXB<{4Hl7u?!H@T%?hZK?uN=ClX@mYj@1Zn{Sl z=iq_~=*18)5K7;ii>)5T;jNg!gHgHVwqA!=%%tzgW255Gb*;kh>9bRrohoWtiag}? z7p*Ga?O0plkBETC0~Wnm|J9*{q=*5imDT#n0;n9JrIr;R&$X?dNRP;8=cp=b1j9t# z&t7T+d*(fHAE8QC3d)KF?sP{Eho7xEdqz;|S+x!;>5I7R{>EEyOUyH$d^hmZo~GJJ zhx~uu0cZkDnCxhg&VDc5`jj(u{_(jx_9=om=ZeufxPRFP&f% zYPeLM*PJ*SE_y4_6+|4pxn6gJo<$Z5lvE{q7^V>mQ2Dt+f4iWw0#2hAgCA7Ifw=?% z(125IH_tNjh_nR$tber{GKiOl7u3TUVT%={?Q@Nbm)?A&0A)Y}Y?+YFbOp9g0NX4i zelf1s$$)Y@dR1T4$k3E4hp~$NvQo*8IPNLUH=GneROJ4Ndnc3n+|78lcv7>>9sTN+ z{ngC5rpO*>UHG7DwI-b#dM6)FlNYhpUBa-Fz>gWGz^Zn3D^fqGw zolfEWcjLfvSe0!2d0!?EP3jEP!OP0=AVXr-;;sQ{hfrPsv?RI{8DSonE(y30du zq$8O&=FemYZ$y7+?0+=j_+S}0!dA|Wv)5JFrZFTr++6+aJNdkyiYs@}xNzVluX9F! zyzsK+G{JdGN-(ASQd_+U%IKSCZcqL@_+L7oCuXA2*zQ;t^{LIO<6kM2yOt(*^y2b? z1A#kSX$*5s(i$CW4gX?I=guLS-N;qzHT`q}dd_$s2Hp0T=r}Wc(|kr6(%n8F8g_Dq zcdcj)0XmHYy}fmf@wPON+F(oP zu)EZIY9w;aVR6mE*ARX7188W(5>BiEza?XCEFRMO73J+((R}ktElho2II6Iy_wLX` znwUS{Jit5eB7nW3yvlo&pJNmwV(I2w+dx!o@IVlp=CoNQwpm5$qRwtLiPg8Lo-So$ z)9ht0>8n~Pas#DyVg@VbkCsdoP5nXNY{A^Ne0|+Jcm>;HmM>kDGglu#MjDi04ek&Z zP9-IdlbD}`o+g5?JvUT~hf8gB+A6AMJ<=vpXQ~N{=)!7sr*KvTskp)UoK*R`#ORm= zA>SZ7X!3S8l1|pH{0=2^@g+xLqQgyrbL;NYGL0N7f=J`R*?kpM+?J3UGF(G#){W!! z{pSO^BIpaIagP(1lGsdjtE+iL>^VRz*ajYk1R^a(2&X^Uo2KIQC&ixi2}Lwl34)PH zhXBq1fxYuQ3*yaheGysI104vDG_Lz|1&&4d*%Ub9l zh%mH3@^#Y2^r3EzBJP(-5YMebQ;S9)h;5V`L|9V@R(xDYU!wyXdIdtffU3hCPVO?l zTn_X(4752IzNlAoRd@@}`2h0zh2$suLl;9le$h)+aePy@x&LWHFcFBd;=P$ekCQ(m zp!@Dlg(pUw*z`VV7}%J^=Cp7w-P&I$B%;6!0k`)aLXmGi@t@4t_B)Wee?M_qp~`n5 zQIufc5Lp5*_ETie?JzqNtJ#G$TTHrf(0V2jDX6luG-%^%BZ3tw&%hen`Uat<_|<3N z5_N?;ti;kZ@h!6fIMNw}kbQNmc0Sf<7}c{OwZu3MxuiApHy?n{V8HsU0m zMp@|;ffczyh=NJBy>`JwWmuGYaZ=Gc5rtgS-uD~}sku2d#gm7V2NXTm zTWP$(#rAZ2qW|8(+_Q84DEuhRWKPm!o{|)BJA_`&_Y8yp_9l%1b*X934{2`1ln2Jx zjwh`a$-`bB+&Xd?2x-@_B(zcA>+Ea}jirreX-793j5$1IgLcRZIW7~YH*bU&&)IESjUxoSAlUbmW2p|#sT1Udmbys;b< zJJ`x2Xh#Kj1)nM#m7OI^kK@}=7mhec71&7l=Km;u@vx@%d|W@m_VODE97VG^XoDIn zJoH$Lc$2S~Dk>u<_Z&ReXds9GcPOnHK=ockHa@NAZq?~@7$&N^K0Di4v95H{+N{a6 z2%IE>fX&6TpI*gfUm`rWZWV5Ya&3$s* zQPEq}(fs~B`U`MFOSftW5EJyv80f-5NSiW!9E9kC_bC>M^Ui2;IOO$1Y{ba;#_WHQ zBQEW8NW&n_=?{4W$QLlz9RUN;&@n$TEY}}e4;RmR5#maBL4GA+1R!lNz6fDp*J}LA zxJ{j&HWnVh@`a5p~dUgwcQY$)H;n#Z}=7+l&jn9 zahYfv!U&!#F|$ckDm}c}t63s~FD$xlSETrm!T}8B)>g46)E)qM+6KdS*;=HV_YisZ zodMikBm$?cy>);WNloy*eoX)uahLk&+K5?rnEd0%Iz9r76x7!<)WswyjezVdTiSBX3y(m9Dn#90SQJQNH9i#nGHe1V*>czEam0~A9`EGl#oIbHa*03 z^O;>H+R98c)OeJWgdW^bn)f3B4Pynj^q8iZ)=W|Z@Q0p3aU>)cxZ@wQ1fHT}f*ByL zXW~h;TF?KV%k;>xF~3i)T<*?PG#Zue{75Ezf!^lu!?5lph91zk3t@OWA*3}0u*T(V zLSBs!DEcgKXFbYRr|1n{ex`tV?hQ62JnRf;_8kuUdOHI z;Q8Lk*$R~s)qa)4`Fi4+^V0tEe3(cn8 znY)xC6=x6lPJy#tTU$aER`HKwcZZV$;`uDyitbM0NvS61#0_~TPgX#us6k;qC&RUg z)0-`cjkn`cSOe^C`hy?%s>W50t(%Sqsw3D+_-y9{v?`=g@xU43C#LluYS^I)Wp~_P zeqD_hk55~4pR9*}-kWuVR2h6=%LuD#wENji*L@wHCJc<<#ioN0NG0e%0AnTY9um=# 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 S0$)o*`yinpUi{wB?|%U Date: Sat, 10 May 2014 23:55:28 +0200 Subject: [PATCH 24/29] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 84d11ef..2daf2e3 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,14 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen * Equations are converted to LaTex equations and surrounded by ``$`` signs * Table of contents: * Is replaced by `[[TOC]]` - * Line breaks: + * Horizontal line: * Inserts a `---` + * Header/Footer: + * Extracts text with all formattings and inserts it at the top and bottom of the markdown document, seperated by `---`. * Tables: - * Converted to Markdown tables following GitHub Markdown syntax. + * Converted to Markdown tables following GitHub Markdown syntax. Formatting within cells gets transferred. * Source code: - * Fenced code blocks are started by three back-ticks and a string and ended by three back-ticks. Single line break is used within fenced code blocks. + * Fenced code blocks are started by three back-ticks and a string and ended by three back-ticks. If such a fenced code block is detected, single line break is used within it. ## CONTRIBUTORS From 6f59392952a7eccc0960dd3cf55164c90ebdae57 Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Sat, 10 May 2014 23:56:28 +0200 Subject: [PATCH 25/29] Documentation update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2daf2e3..73d91a6 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen * Images: * images are correctly extracted and sent as attachments * Drawings: - * not supported + * not supported - there seems to be no API function to export a drawing as a rasterized or vector image. * Equations: * Equations are converted to LaTex equations and surrounded by ``$`` signs * Table of contents: From af9be0c1950154edde7821caeac233629ddf91f5 Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Sun, 11 May 2014 00:15:07 +0200 Subject: [PATCH 26/29] Do not include empty header and footer regions --- converttomarkdown.gapps | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 077399c..f00bc99 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -132,15 +132,17 @@ function ConvertToMarkdownFile() { function processSection(section) { var state = { - 'inSource' : false, - 'images' : [], - 'imageCounter' : 0, - 'prevDoc' : [], - 'nextDoc' : [], - 'inSource' : false, - 'size' : [], + 'inSource' : false, // Document read pointer is within a fenced code block + 'images' : [], // Image data found in document + 'imageCounter' : 0, // Image counter + 'prevDoc' : [], // Pointer to the previous element on aparsing tree level + 'nextDoc' : [], // Pointer to the next element on a parsing tree level + 'size' : [], // Number of elements on a parsing tree level }; + + // Process element tree outgoing from the root element var textElements = processElement(section, state, 0); + return { 'textElements' : textElements, 'state' : state, @@ -155,11 +157,14 @@ function markdown() { // Process header var head = DocumentApp.getActiveDocument().getHeader(); if(head != null) { + // Do not include empty header sections var teHead = processSection(head); - textElements = textElements.concat(teHead.textElements); - textElements.push('\n\n'); - textElements.push('---'); - textElements.push('\n\n'); + if(teHead.length > 0) { + textElements = textElements.concat(teHead.textElements); + textElements.push('\n\n'); + textElements.push('---'); + textElements.push('\n\n'); + } } // Process body @@ -172,10 +177,13 @@ function markdown() { Logger.log("foot: " + foot); if(foot != null) { var teFoot = processSection(foot); - textElements.push('\n\n'); - textElements.push('---'); - textElements.push('\n\n'); - textElements = textElements.concat(teFoot.textElements); + // Do not include empty footer sections + if(teFoot.length > 0) { + textElements.push('\n\n'); + textElements.push('---'); + textElements.push('\n\n'); + textElements = textElements.concat(teFoot.textElements); + } } // Build final output string @@ -604,4 +612,3 @@ function processElement(element, state, depth) { return textElements; } - From 5f269d8f6783cd5f971547a230b0a15c3faa778a Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Sun, 11 May 2014 01:18:25 +0200 Subject: [PATCH 27/29] Ensure spaces before and after MD formattings --- converttomarkdown.gapps | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index f00bc99..0a90962 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -159,7 +159,7 @@ function markdown() { if(head != null) { // Do not include empty header sections var teHead = processSection(head); - if(teHead.length > 0) { + if(teHead.textElements.length > 0) { textElements = textElements.concat(teHead.textElements); textElements.push('\n\n'); textElements.push('---'); @@ -178,7 +178,7 @@ function markdown() { if(foot != null) { var teFoot = processSection(foot); // Do not include empty footer sections - if(teFoot.length > 0) { + if(teFoot.textElements.length > 0) { textElements.push('\n\n'); textElements.push('---'); textElements.push('\n\n'); @@ -282,10 +282,28 @@ function handleTable(element, state, depth) { table = buildTable(Math.max(10, table.maxSize + 1)); textElements = textElements.concat(table.stack); - textElements.push("\n"); + textElements.push('\n'); return textElements; } +function formatMd(text, indexLeft, formatLeft, indexRight, formatRight) { + var leftPad = '' + formatLeft; + if(indexLeft > 0) { + if(text[indexLeft - 1] != ' ') + leftPad = ' ' + formatLeft; + } + + var rightPad = formatRight + ''; + if(indexRight < text.length) { + if(text[indexRight] != ' ') { + rightPad = formatRight + ' '; + } + } + + var formatted = text.substring(0, indexLeft) + leftPad + text.substring(indexLeft, indexRight) + rightPad + text.substring(indexRight); + return formatted; +} + function handleText(doc, state) { var formatted = doc.getText(); @@ -306,6 +324,9 @@ function handleText(doc, state) { url = txt.getLinkUrl(off); } formatted = formatted.substring(0, index) + '[' + formatted.substring(index, lastIndex) + '](' + url + ')' + formatted.substring(lastIndex); + + // Do not handle additional formattings for links + continue; } // Handle font family @@ -315,12 +336,15 @@ function handleText(doc, state) { if (!state.inSource && font === sourceFont) { // Scan left until text without source font is found - while (i > 0 && txt.getFontFamily(attrs[i-1]) && txt.getFontFamily(attrs[i-1]) === sourceFont) { + while (i > 0 && doc.getFontFamily(attrs[i-1]) && doc.getFontFamily(attrs[i-1]) === sourceFont) { i -= 1; off = attrs[i]; } - formatted = formatted.substring(0, lastIndex) + '`' + formatted.substring(index, lastIndex) + '`' + formatted.substring(lastIndex); + formatted = formatMd(formatted, index, '`', lastIndex, '`'); + + // Do not handle additional formattings for code + continue; } } @@ -334,12 +358,12 @@ function handleText(doc, state) { dleft = "**_"; dright = "_**"; } - formatted = formatted.substring(0, index) + dleft + formatted.substring(index, lastIndex) + dright + formatted.substring(lastIndex); + formatted = formatMd(formatted, index, dleft, lastIndex, dright); } // Handle italic else if(doc.isItalic(index)) { - formatted = formatted.substring(0, index) + '*' + formatted.substring(index, lastIndex) + '*' + formatted.substring(lastIndex); + formatted = formatMd(formatted, index, '*', lastIndex, '*'); } // Keep track of last position in text From 28588ca126d5d26af0306c8c88c00a2671d936c3 Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Sun, 6 Jul 2014 15:55:57 +0200 Subject: [PATCH 28/29] Fixed list counters Added lsitCounters element to the state object --- converttomarkdown.gapps | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 0a90962..3218b56 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -138,6 +138,7 @@ function processSection(section) { 'prevDoc' : [], // Pointer to the previous element on aparsing tree level 'nextDoc' : [], // Pointer to the next element on a parsing tree level 'size' : [], // Number of elements on a parsing tree level + 'listCounters' : [], // List counter }; // Process element tree outgoing from the root element @@ -389,18 +390,23 @@ function handleListItem(item, state, depth) { // Add marker based on glyph type var glyph = item.getGlyphType(); + Logger.log("Glyph: " + glyph); switch(glyph) { case DocumentApp.GlyphType.BULLET: case DocumentApp.GlyphType.HOLLOW_BULLET: case DocumentApp.GlyphType.SQUARE_BULLET: prefix += '* '; - break; - default: + break; + case DocumentApp.GlyphType.NUMBER: var key = item.getListId() + '.' + item.getNestingLevel(); + Logger.log("key " + key); var counter = state.listCounters[key] || 0; counter++; state.listCounters[key] = counter; prefix += counter + '. '; + default: + prefix += '* '; + break; } // Add prefix @@ -635,4 +641,3 @@ function processElement(element, state, depth) { return textElements; } - From 8bfd8953aff7eb2a744c3582348f1ab7de8f9e9f Mon Sep 17 00:00:00 2001 From: Andreas Wolke Date: Sun, 6 Jul 2014 15:58:39 +0200 Subject: [PATCH 29/29] Added break statement to fix numbered list counter --- converttomarkdown.gapps | 1 + 1 file changed, 1 insertion(+) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 3218b56..941a1d5 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -404,6 +404,7 @@ function handleListItem(item, state, depth) { counter++; state.listCounters[key] = counter; prefix += counter + '. '; + break; default: prefix += '* '; break;