diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/README.md b/README.md index 73d91a6..d41fefe 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,23 @@ A simple Google Apps script to convert a properly formatted Google Drive Documen ![Screenshot Google Docs with gdocs2md](markdown.png) +## :warning: Updates on this repo + +Proceed carefully. Take some time to experiment and learn how it works. I take no responsibility for lost documents. + +This repo introduces 2 new versions for the file export feature: +- v1.5 where you specify the destination folder (need to edit the first variable on the script file) +- v2 where it saves the files on the same folder where the google document is located. + +Both versions update files instead deleting an entire subfolder. (with exception to images, they are actually deleted and reuploaded). +Hope this is useful for other ppl. If you're curious I'm using this to be able to get a regular backup on github of an important document I'm working on :) + +#### Known limitations: +Under some circumstances the script will fail. I've been trying to fix some myself, for others I've been asking around for help. +Current known limitations that I am not working on include: +- the use of columns +- the use of page numbers + ## Usage * Adding this script to your doc (once per doc): @@ -51,6 +68,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) +* Omar Costa Hamido - [webpage](https://omarcostahamido.com) - [Github](https://github.com/omarcostahamido) ## LICENSE diff --git a/converttomarkdown-v2.gapps b/converttomarkdown-v2.gapps new file mode 100644 index 0000000..b3c6bc4 --- /dev/null +++ b/converttomarkdown-v2.gapps @@ -0,0 +1,650 @@ +// Open handler to add Menu +function onOpen(e) { + var ui = DocumentApp.getUi(); + + 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(); + } +} + +function onInstall(e) { + onOpen(e); +} + +function ConvertEquation() { + var element = DocumentApp.getActiveDocument().getCursor().getElement(); + + // 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!"); + return; + } + + // Covert equation + var latexEquation = handleEquationFunction(element); + var latexEquationText = "$" + latexEquation.trim() + "$"; + + // Show results + DocumentApp.getUi().alert(latexEquationText); +} + +// Convert current document to markdown and email it +function ConvertToMarkdownEmail() { + // Convert to markdown + var convertedDoc = markdown(); + + // 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(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", + { "attachments": convertedDoc.attachments }); +} + + +// Convert current document to file and save it to GDrive +function ConvertToMarkdownFile() { + // Convert to markdwon + var convertedDoc = markdown(); + + // Create folder + var id = DocumentApp.getActiveDocument().getId(); + var file = DriveApp.getFileById(id); + var parents = file.getParents(); + + /* + // if the folder is on a computer backup location it will hang on this process. Go figure! + var howmanyparents = 0; + + while(parents.hasNext()){ + howmanyparents++; + //DocumentApp.getUi().alert("total number of parents: "+howmanyparents); + } + + if(howmanyparents > 1) { + Logger.log("File has multiple parent directory. Script does not work in this case"); + DocumentApp.getUi().alert("Document must not be in multiple directories"); + return; + } + + if(howmanyparents == 0) { + DocumentApp.getUi().alert("Document has to be in a directory for the export"); + return; + } + */ + + // Use first parent + var parent = parents.next(); + + // Create new target folder + //var found = parent.createFolder("target"); + //var found = DriveApp.getFolderById('YOUR_FOLDER_ID_HERE'); + + // 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; + + while(parent.getFilesByName(name).hasNext()){ + var currentimagefile = parent.getFilesByName(name).next(); + if(currentimagefile.getName() == name){ + currentimagefile.setTrashed(true); + } + } + blob.setName(name); + parent.createFile(blob); + } + + // Write mardown file to target folder + var filename = DocumentApp.getActiveDocument().getName() + ".md"; + + while(parent.getFilesByName(filename).hasNext()){ + var currentfile = parent.getFilesByName(filename).next(); + if(currentfile.getName() == filename){ + currentfile.setContent(convertedDoc.text) + return + } + } + + parent.createFile(filename, convertedDoc.text, "text/plain"); +} + +function processSection(section) { + var state = { + '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 + 'listCounters' : [], // List counter + }; + + // Process element tree outgoing from the root element + 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) { + // Do not include empty header sections + var teHead = processSection(head); + if(teHead.textElements.length > 0) { + 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); + // Do not include empty footer sections + if(teFoot.textElements.length > 0) { + 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 + text = text.replace('\u201d', '"').replace('\u201c', '"'); + + // Debug logging + Logger.log("Result: " + text); + Logger.log("Images: " + doc.state.imageCounter); + + // Build attachment and file lists + var attachments = []; + var files = []; + for(var i in doc.state.images) { + var image = doc.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, + }; +} + + +function escapeHTML(text) { + return text.replace(//g, '>'); +} + +// Add repeat function to strings +String.prototype.repeat = function( num ) { + return new Array( num + 1 ).join( this ); +} + +function handleTable(element, state, depth) { + var textElements = []; + + 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; +} + +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(); + 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); + + // Do not handle additional formattings for links + continue; + } + + // 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 && doc.getFontFamily(attrs[i-1]) && doc.getFontFamily(attrs[i-1]) === sourceFont) { + i -= 1; + off = attrs[i]; + } + + formatted = formatMd(formatted, index, '`', lastIndex, '`'); + + // Do not handle additional formattings for code + continue; + } + } + + // 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 = formatMd(formatted, index, dleft, lastIndex, dright); + } + // Handle italic + else if(doc.isItalic(index)) { + formatted = formatMd(formatted, index, '*', 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)?doc.getChild(i-1) : child; + state.prevDoc[depth] = prevDoc; + + textElements = textElements.concat(processElement(child, state, depth+1)); + } + return textElements; +} + + +function processElement(element, state, depth) { + // Result + var textElements = []; + + switch(element.getType()) { + case DocumentApp.ElementType.DOCUMENT: + Logger.log("this is a document"); + break; + + case DocumentApp.ElementType.BODY_SECTION: + textElements = textElements.concat(processChilds(element, state, depth)); + break; + + case DocumentApp.ElementType.PARAGRAPH: + // Determine header prefix + var prefix = ''; + switch (element.getHeading()) { + // Add a # for each heading level. No break, so we accumulate the right number. + case DocumentApp.ParagraphHeading.HEADING6: prefix += '#'; + case DocumentApp.ParagraphHeading.HEADING5: prefix += '#'; + case DocumentApp.ParagraphHeading.HEADING4: prefix += '#'; + case DocumentApp.ParagraphHeading.HEADING3: prefix += '#'; + case DocumentApp.ParagraphHeading.HEADING2: prefix += '#'; + case DocumentApp.ParagraphHeading.HEADING1: prefix += '#'; + } + + // Add space + if(prefix.length > 0) + prefix += ' '; + + // Push prefix + textElements.push(prefix); + + // Process childs + 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) + break; + + if(state.inSource) + textElements.push('\n'); + else + textElements.push('\n\n'); + + break; + + case DocumentApp.ElementType.LIST_ITEM: + textElements = textElements.concat(handleListItem(element, state, depth)); + textElements.push('\n'); + + if(state.nextDoc[depth-1].getType() != element.getType()) { + textElements.push('\n'); + } + + break; + + case DocumentApp.ElementType.HEADER_SECTION: + textElements = textElements.concat(processChilds(element, state, depth)); + break; + + case DocumentApp.ElementType.FOOTER_SECTION: + textElements = textElements.concat(processChilds(element, state, depth)); + break; + + case DocumentApp.ElementType.FOOTNOTE: + textElements.push(' (NOTE: '); + textElements = textElements.concat(processChilds(element.getFootnoteContents(), state, depth)); + textElements.push(')'); + break; + + case DocumentApp.ElementType.HORIZONTAL_RULE: + textElements.push('---\n'); + break; + + case DocumentApp.ElementType.INLINE_DRAWING: + // Cannot handle this type - there is no export function for rasterized or SVG images... + break; + + case DocumentApp.ElementType.TABLE: + textElements = textElements.concat(handleTable(element, state, depth)); + break; + + case DocumentApp.ElementType.TABLE_OF_CONTENTS: + textElements.push('[[TOC]]'); + break; + + 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; + + case DocumentApp.ElementType.INLINE_IMAGE: + textElements = textElements.concat(handleImage(element, state)); + break; + + case DocumentApp.ElementType.PAGE_BREAK: + // Ignore page breaks + break; + + case DocumentApp.ElementType.EQUATION: + var latexEquation = handleEquationFunction(element, state); + + // 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: + throw("Unknown element type: " + element.getType()); + } + + return textElements; +} diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 941a1d5..796798c 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -75,26 +75,32 @@ function ConvertToMarkdownFile() { // Create folder var id = DocumentApp.getActiveDocument().getId(); - var file = DocsList.getFileById(id); + var file = DriveApp.getFileById(id); var parents = file.getParents(); - if(parents.length > 1) { + var howmanyparents = 0; + + while(parents.hasNext()){ + howmanyparents++ + } + + if(howmanyparents > 1) { Logger.log("File has multiple parent directory. Script does not work in this case"); DocumentApp.getUi().alert("Document must not be in multiple directories"); return; } - if(parents.length == 0) { + if(howmanyparents == 0) { DocumentApp.getUi().alert("Document has to be in a directory for the export"); return; } // Use first parent - var parent = parents[0]; + var parent = parents.next(); // Check if target folder exists - for(var folder in parent.getFolders()) { - folder = parent.getFolders()[folder]; + while(parent.getFolders().hasNext()) { + var folder = parent.getFolders().next(); if(folder.getName() == 'target') { var ui = DocumentApp.getUi(); diff --git a/converttomarkdown_v1.5-dest-folder.gapps b/converttomarkdown_v1.5-dest-folder.gapps new file mode 100644 index 0000000..98fd786 --- /dev/null +++ b/converttomarkdown_v1.5-dest-folder.gapps @@ -0,0 +1,632 @@ +// Global variable your selected destination folder +var dstfldr = 'YOUR_FOLDER_ID_HERE'; + +// Open handler to add Menu +function onOpen(e) { + var ui = DocumentApp.getUi(); + + 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(); + } +} + +function onInstall(e) { + onOpen(e); +} + +function ConvertEquation() { + var element = DocumentApp.getActiveDocument().getCursor().getElement(); + + // 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!"); + return; + } + + // Covert equation + var latexEquation = handleEquationFunction(element); + var latexEquationText = "$" + latexEquation.trim() + "$"; + + // Show results + DocumentApp.getUi().alert(latexEquationText); +} + +// Convert current document to markdown and email it +function ConvertToMarkdownEmail() { + // Convert to markdown + var convertedDoc = markdown(); + + // 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(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", + { "attachments": convertedDoc.attachments }); +} + + +// Convert current document to file and save it to GDrive +function ConvertToMarkdownFile() { + // Convert to markdwon + var convertedDoc = markdown(); + + // Create folder + var id = DocumentApp.getActiveDocument().getId(); + var file = DriveApp.getFileById(id); + var parents = file.getParents(); + + // Find the target folder + var found = DriveApp.getFolderById(dstfldr); + + // 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; + + while(found.getFilesByName(name).hasNext()){ + var currentimagefile = found.getFilesByName(name).next(); + if(currentimagefile.getName() == name){ + currentimagefile.setTrashed(true); + } + } + blob.setName(name); + found.createFile(blob); + } + + // Write mardown file to target folder + var filename = DocumentApp.getActiveDocument().getName() + ".md"; + + while(found.getFilesByName(filename).hasNext()){ + var currentfile = found.getFilesByName(filename).next(); + if(currentfile.getName() == filename){ + currentfile.setContent(convertedDoc.text) + return + } + } + + found.createFile(DocumentApp.getActiveDocument().getName() + ".md", convertedDoc.text, "text/plain"); +} + +function processSection(section) { + var state = { + '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 + 'listCounters' : [], // List counter + }; + + // Process element tree outgoing from the root element + 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) { + // Do not include empty header sections + var teHead = processSection(head); + if(teHead.textElements.length > 0) { + 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); + // Do not include empty footer sections + if(teFoot.textElements.length > 0) { + 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 + text = text.replace('\u201d', '"').replace('\u201c', '"'); + + // Debug logging + Logger.log("Result: " + text); + Logger.log("Images: " + doc.state.imageCounter); + + // Build attachment and file lists + var attachments = []; + var files = []; + for(var i in doc.state.images) { + var image = doc.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, + }; +} + + +function escapeHTML(text) { + return text.replace(//g, '>'); +} + +// Add repeat function to strings +String.prototype.repeat = function( num ) { + return new Array( num + 1 ).join( this ); +} + +function handleTable(element, state, depth) { + var textElements = []; + + 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; +} + +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(); + 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(attrs[i-1]!=null){ + while(doc.getLinkUrl(attrs[i-1]) === url){ + i--; + index = attrs[i]; + if(attrs[i-1]==null){ + break; + } + } + } + formatted = formatted.substring(0, index) + '[' + formatted.substring(index, lastIndex) + '](' + url + ')' + formatted.substring(lastIndex); + + // Do not handle additional formattings for links + continue; + } + + // 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 && doc.getFontFamily(attrs[i-1]) && doc.getFontFamily(attrs[i-1]) === sourceFont) { + i -= 1; + off = attrs[i]; + } + + formatted = formatMd(formatted, index, '`', lastIndex, '`'); + + // Do not handle additional formattings for code + continue; + } + } + + // 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 = formatMd(formatted, index, dleft, lastIndex, dright); + } + // Handle italic + else if(doc.isItalic(index)) { + formatted = formatMd(formatted, index, '*', 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)?doc.getChild(i-1) : child; + state.prevDoc[depth] = prevDoc; + + textElements = textElements.concat(processElement(child, state, depth+1)); + } + return textElements; +} + + +function processElement(element, state, depth) { + // Result + var textElements = []; + + switch(element.getType()) { + case DocumentApp.ElementType.DOCUMENT: + Logger.log("this is a document"); + break; + + case DocumentApp.ElementType.BODY_SECTION: + textElements = textElements.concat(processChilds(element, state, depth)); + break; + + case DocumentApp.ElementType.PARAGRAPH: + // Determine header prefix + var prefix = ''; + switch (element.getHeading()) { + // Add a # for each heading level. No break, so we accumulate the right number. + case DocumentApp.ParagraphHeading.HEADING6: prefix += '#'; + case DocumentApp.ParagraphHeading.HEADING5: prefix += '#'; + case DocumentApp.ParagraphHeading.HEADING4: prefix += '#'; + case DocumentApp.ParagraphHeading.HEADING3: prefix += '#'; + case DocumentApp.ParagraphHeading.HEADING2: prefix += '#'; + case DocumentApp.ParagraphHeading.HEADING1: prefix += '#'; + } + + // Add space + if(prefix.length > 0) + prefix += ' '; + + // Push prefix + textElements.push(prefix); + + // Process childs + 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) + break; + + if(state.inSource) + textElements.push('\n'); + else + textElements.push('\n\n'); + + break; + + case DocumentApp.ElementType.LIST_ITEM: + textElements = textElements.concat(handleListItem(element, state, depth)); + textElements.push('\n'); + + if(state.nextDoc[depth-1].getType() != element.getType()) { + textElements.push('\n'); + } + + break; + + case DocumentApp.ElementType.HEADER_SECTION: + textElements = textElements.concat(processChilds(element, state, depth)); + break; + + case DocumentApp.ElementType.FOOTER_SECTION: + textElements = textElements.concat(processChilds(element, state, depth)); + break; + + case DocumentApp.ElementType.FOOTNOTE: + textElements.push(' (NOTE: '); + textElements = textElements.concat(processChilds(element.getFootnoteContents(), state, depth)); + textElements.push(')'); + break; + + case DocumentApp.ElementType.HORIZONTAL_RULE: + textElements.push('---\n'); + break; + + case DocumentApp.ElementType.INLINE_DRAWING: + // Cannot handle this type - there is no export function for rasterized or SVG images... + break; + + case DocumentApp.ElementType.TABLE: + textElements = textElements.concat(handleTable(element, state, depth)); + break; + + case DocumentApp.ElementType.TABLE_OF_CONTENTS: + textElements.push('[[TOC]]'); + break; + + 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; + + case DocumentApp.ElementType.INLINE_IMAGE: + textElements = textElements.concat(handleImage(element, state)); + break; + + case DocumentApp.ElementType.PAGE_BREAK: + // Ignore page breaks + break; + + case DocumentApp.ElementType.EQUATION: + var latexEquation = handleEquationFunction(element, state); + + // 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: + throw("Unknown element type: " + element.getType()); + } + + return textElements; +}