// vim: set softtabstop=2 shiftwidth=2 expandtab : var ADOC_MIMETYPE = "text/asciidoc"; // Open handler to add Menu function onOpen(e) { var ui = DocumentApp.getUi(); if (e && e.authMode == ScriptApp.AuthMode.NONE) { ui.createMenu('AsciiDoc') // .addItem('Latex Equation', 'ConvertEquation') .addToUi(); } else { ui.createMenu('AsciiDoc') .addItem('Export File', 'ConvertToAsciiDocFile') .addItem('Export Email', 'ConvertToAsciiDocEmail') // .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 asciidoc and email it function ConvertToAsciiDocEmail() { // Convert to asciidoc var convertedDoc = asciidoc(); // Add asciidoc document to attachments convertedDoc.attachments.push({"fileName":DocumentApp.getActiveDocument().getName()+".adoc", "mimeType": ADOC_MIMETYPE, "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 asciidoc document MailApp.sendEmail(mail, "[gdoc2adoc] "+DocumentApp.getActiveDocument().getName(), "Your converted AsciiDoc document is attached (converted from "+DocumentApp.getActiveDocument().getUrl()+")", { "attachments": convertedDoc.attachments }); } function getFolder(parent_folder,folder_name){ var folders = parent_folder.getFolders(); while (folders.hasNext()) { var folder = folders.next(); if(folder_name == folder.getName()) { return folder; } } return false; } // Convert current document to file and save it to GDrive function ConvertToAsciiDocFile() { // Convert to asciidoc var convertedDoc = asciidoc(); // Create folder var id = DocumentApp.getActiveDocument().getId(); var file = DriveApp.getFileById(id); var parents = file.getParents(); var parent = null; if(parents.hasNext()) { parent = parents.next() if(parents.hasNext()) { //Logger.log("File has multiple parent directories. Script does not work in this case"); DocumentApp.getUi().alert("Document must not be in multiple directories"); return; } } else { DocumentApp.getUi().alert("Document has to be in a directory for the export"); return; } var targetDirName = DocumentApp.getActiveDocument().getName() + ".adoc" var found = getFolder(parent,targetDirName); if (found === false){ //Logger.log("Creating output folder..."); found = parent.createFolder(targetDirName); } else { //Logger.log("Reusing existing target"); var ui = DocumentApp.getUi(); var result = ui.alert('Existing target folder found!', 'You already have "' + targetDirName + '" in your Drive. Delete folder or Cancel ?', ui.ButtonSet.OK_CANCEL) if(result == ui.Button.OK) { //Logger.log("Trashing target folder..."); found.setTrashed(true); //Logger.log("Re-Creating output folder..."); found = parent.createFolder(targetDirName); } else { //Logger.log("Do not delete target folder, stopping!"); return; } } // 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 // using same nameas folder. found.createFile(targetDirName, convertedDoc.text, ADOC_MIMETYPE); } 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 'needToc' : false, // saw a toc reference 'indent' : 0, // indent on current block 'headingPrefix' : '', // Should headings be prefixed by a `=` to make heading1 `==` (see detectHeadingRules) }; // Detect title, heading1 etc detectHeadingRules(section, state); // Process element tree outgoing from the root element var textElements = processElement(section, state, 0); return { 'textElements' : textElements, 'state' : state, }; } function detectHeadingRulesNested(element, state, headingState) { Logger.log("detectHeadingRulesNested: " + element.getType()); switch(element.getType()) { case DocumentApp.ElementType.DOCUMENT: case DocumentApp.ElementType.BODY_SECTION: // Iterates over all childs for(var i=0; i < element.getNumChildren(); i++) { var child = element.getChild(i); detectHeadingRulesNested(child, state, headingState); } break; case DocumentApp.ElementType.PARAGRAPH: switch (element.getHeading()) { case DocumentApp.ParagraphHeading.HEADING1: headingState.nbrOfH1 += 1; break; case DocumentApp.ParagraphHeading.TITLE: headingState.hasTitle = true; } // Iterates over all childs for(var i=0; i < element.getNumChildren(); i++) { var child = element.getChild(i); detectHeadingRulesNested(child, state, headingState); } } } function detectHeadingRules(element, state) { var headingState = { 'nbrOfH1': 0, 'hasTitle' : false, }; detectHeadingRulesNested(element, state, headingState); Logger.log("Heading detection, nbrOfH1: " + headingState.nbrOfH1 + " and title: " + headingState.hasTitle); if (headingState.hasTitle == true) { Logger.log("Heading prefix `=` because there is a title"); state.headingPrefix = '='; } else if (headingState.nbrOfH1 == 1) { // we have a single heading1 so we will pretend it's the title and h2 is for subsections Logger.log("Heading prefix `` because there is a single h1"); state.headingPrefix = ''; } else { // no title and multiple (or no h1) => treat h1 as a Asciidoc subsection (i.e. `==`) Logger.log("No title and multiple (or no h1) => treat h1 as a Asciidoc subsection (i.e. `==`)"); state.headingPrefix = '='; } } function asciidoc() { // 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"); 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") 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 + ' '; } } //Note: we do a trim via .replace since asciidoc wont format i.e. * test* with bold. var formatted = text.substring(0, indexLeft) + leftPad + text.substring(indexLeft, indexRight).replace(/^\s+|\s+$/g,"") + 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 = doc.getLinkUrl(index); } formatted = formatted.substring(0, index) + url + '[' + formatted.substring(index, lastIndex) + ']' + formatted.substring(lastIndex); // Do not handle additional formattings for links continue; } // Handle source code if(isTextCode(doc, index)) { if (!state.inSource) { // 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; } } // TODO: does not handle nested formatting well. // 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, '_'); } else if (doc.isUnderline(index)) { formatted = formatMd(formatted, index, '[.underline]#', lastIndex, '#'); } else if(doc.isStrikethrough(index)) { formatted = formatMd(formatted, index, '[.line-through]#', lastIndex, '#'); } else if(doc.getTextAlignment(index) == DocumentApp.TextAlignment.SUBSCRIPT) { formatted = formatMd(formatted, index, '~', lastIndex, '~'); } else if(doc.getTextAlignment(index) == DocumentApp.TextAlignment.SUPERSCRIPT) { formatted = formatMd(formatted, index, '^', lastIndex, '^'); } // replace newlines wit any indent requested from higher up formatting. var haslinebreak = /\r/.exec(formatted); formatted = formatted.replace(/(?:\r)/g, '\n+\n'); if (haslinebreak) // add extra newline to let list and others continue formatted += "\n"; // 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()) { case DocumentApp.ParagraphHeading.HEADING6: prefix = state.headingPrefix + '======'; break; case DocumentApp.ParagraphHeading.HEADING5: prefix = state.headingPrefix + '====='; break; case DocumentApp.ParagraphHeading.HEADING4: prefix = state.headingPrefix + '===='; break; case DocumentApp.ParagraphHeading.HEADING3: prefix = state.headingPrefix + '==='; break; case DocumentApp.ParagraphHeading.HEADING2: prefix = state.headingPrefix + '=='; break; case DocumentApp.ParagraphHeading.HEADING1: prefix = state.headingPrefix + '='; break; case DocumentApp.ParagraphHeading.TITLE: prefix = '='; state.needToc = true; break; //default:prefix = element.getHeading(); break; } // Add space if(prefix.length > 0) prefix += ' '; // Process childs var child = processChilds(element, state, depth) // Only Push prefix if it has kids otherwise it will just be treated as lone =='s if (child.length > 0) { textElements.push(prefix); textElements = textElements.concat(child); if(state.needToc) { textElements.push("\n:toc: macro\n") state.needToc = false; } // look for positioned images var positionedImages = element.getPositionedImages() for(var image in positionedImages) { image = positionedImages[image]; textElements.push("\n" + handleImage(image, state) + "\n") } } // 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: //adoced textElements.push('.footnote:['); 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::[]'); state.needToc = true 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 https://github.com/Mogztter/asciidoc-googledocs-addon/blob/master/app/Code.gs /** Guess if the text is code by looking at the font family. */ function isTextCode(doc, index) { // Things will be better if Google Fonts can tell us about a font var i, fontFamily = doc.getFontFamily(index), /* Now it returns a string! */ monospaceFonts = ['Consolas', 'Courier New', 'Source Code Pro']; if (fontFamily === null) { return false; // Handle null early.. It means multiple values. } // See ES7 Array.prototype.includes(elem, pos) for (i = 0; i < monospaceFonts.length; i++) { if (fontFamily === monospaceFonts[i]) { return true; } } // Last Try: Assume it's mono if it ends with ' Mono'. // This works for all Google Fonts as of 2016-10-21. // See ES6 String.prototype.endsWith(str, pos). return fontFamily.indexOf(' Mono') === fontFamily.length - 5; }