From eceaae6a4711651090cf7916f29e1f58dc51625f Mon Sep 17 00:00:00 2001 From: omarcostahamido Date: Sat, 3 Oct 2020 19:51:39 -0700 Subject: [PATCH 01/10] fix deprecated DocsList replaced by DriveApp --- converttomarkdown.gapps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 941a1d5..3530e4f 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -75,7 +75,7 @@ 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) { From ed5d015c2f69fb632a987e75a67f6b859b8b3922 Mon Sep 17 00:00:00 2001 From: omarcostahamido Date: Sat, 3 Oct 2020 21:31:04 -0700 Subject: [PATCH 02/10] fix way to return first parent --- converttomarkdown.gapps | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 3530e4f..88566d3 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -90,11 +90,11 @@ function ConvertToMarkdownFile() { } // 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(); From 10c6e37d40f9c0d1e97b34e6dd8efe88c38f7fed Mon Sep 17 00:00:00 2001 From: omarcostahamido Date: Sat, 3 Oct 2020 21:31:49 -0700 Subject: [PATCH 03/10] new way to figure out number of parents --- converttomarkdown.gapps | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/converttomarkdown.gapps b/converttomarkdown.gapps index 88566d3..796798c 100644 --- a/converttomarkdown.gapps +++ b/converttomarkdown.gapps @@ -78,13 +78,19 @@ function ConvertToMarkdownFile() { 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; } From 25b8b451f8288b3c7e4ff2d29b8998fbbe7354ce Mon Sep 17 00:00:00 2001 From: omarcostahamido Date: Sun, 4 Oct 2020 14:27:22 -0700 Subject: [PATCH 04/10] v1.5 dest-folder a versio where you specify the destination folder. --- converttomarkdown_v1.5-dest-folder.gapps | 628 +++++++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 converttomarkdown_v1.5-dest-folder.gapps diff --git a/converttomarkdown_v1.5-dest-folder.gapps b/converttomarkdown_v1.5-dest-folder.gapps new file mode 100644 index 0000000..669489e --- /dev/null +++ b/converttomarkdown_v1.5-dest-folder.gapps @@ -0,0 +1,628 @@ +// 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 (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; +} From e49803d94e976941f94b9f2e47334849f23ed082 Mon Sep 17 00:00:00 2001 From: omarcostahamido Date: Sun, 4 Oct 2020 14:28:17 -0700 Subject: [PATCH 05/10] v2 updated version, saves on the current folder. updates files instead deleting an entire subfolder. --- converttomarkdown-v2.gapps | 650 +++++++++++++++++++++++++++++++++++++ 1 file changed, 650 insertions(+) create mode 100644 converttomarkdown-v2.gapps 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; +} From 1f8aee95ac0ab3201b65f417cc9c91aca5e2ebe6 Mon Sep 17 00:00:00 2001 From: Omar Costa Hamido Date: Sun, 4 Oct 2020 14:35:28 -0700 Subject: [PATCH 06/10] add updates and notes on the new script versions --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 73d91a6..5e88540 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,17 @@ 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 :) + ## Usage * Adding this script to your doc (once per doc): From 325e1f5b9add08dd6c55d69d9f666ea0a725266d Mon Sep 17 00:00:00 2001 From: omarcostahamido Date: Sun, 4 Oct 2020 20:27:23 -0700 Subject: [PATCH 07/10] Create .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store From ace60810a9e56240eea71db668a0cb952c83f0bb Mon Sep 17 00:00:00 2001 From: Omar Costa Hamido Date: Tue, 23 Feb 2021 11:28:19 -0800 Subject: [PATCH 08/10] fixed links with italics bug it won't save the italics... but it also won't crash --- converttomarkdown_v1.5-dest-folder.gapps | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/converttomarkdown_v1.5-dest-folder.gapps b/converttomarkdown_v1.5-dest-folder.gapps index 669489e..2a2fc52 100644 --- a/converttomarkdown_v1.5-dest-folder.gapps +++ b/converttomarkdown_v1.5-dest-folder.gapps @@ -303,10 +303,9 @@ function handleText(doc, state) { // 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; + while(doc.getLinkUrl(attrs[i-1]) === url){ + i--; index = attrs[i]; - url = txt.getLinkUrl(off); } formatted = formatted.substring(0, index) + '[' + formatted.substring(index, lastIndex) + '](' + url + ')' + formatted.substring(lastIndex); From c936e15959223b1f084ca3919cd6ec45a5dba2ee Mon Sep 17 00:00:00 2001 From: Omar Costa Hamido Date: Wed, 24 Feb 2021 10:24:37 -0800 Subject: [PATCH 09/10] avoid crash when there is no attrs[i-1] --- converttomarkdown_v1.5-dest-folder.gapps | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/converttomarkdown_v1.5-dest-folder.gapps b/converttomarkdown_v1.5-dest-folder.gapps index 2a2fc52..98fd786 100644 --- a/converttomarkdown_v1.5-dest-folder.gapps +++ b/converttomarkdown_v1.5-dest-folder.gapps @@ -303,9 +303,14 @@ function handleText(doc, state) { // Handle links if(doc.getLinkUrl(index)) { var url = doc.getLinkUrl(index); - while(doc.getLinkUrl(attrs[i-1]) === url){ - i--; - index = attrs[i]; + 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); From fa23fc3c33562fcbfc0ba0fdaac09447e56f751b Mon Sep 17 00:00:00 2001 From: Omar Costa Hamido Date: Wed, 9 Feb 2022 23:59:10 +0000 Subject: [PATCH 10/10] known limitations list and add me to contributors --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 5e88540..d41fefe 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ This repo introduces 2 new versions for the file export feature: 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): @@ -62,6 +68,7 @@ Hope this is useful for other ppl. If you're curious I'm using this to be able t * 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