/* 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]...". */ var blogImageDir = "{{blogImageDir}}" /** * Creates a menu entry in the Google Docs UI when the document is opened. * * @param {object} e The event parameter for a simple onOpen trigger. To * determine which authorization mode (ScriptApp.AuthMode) the trigger is * running in, inspect e.authMode. */ function onOpen(e) { DocumentApp.getUi().createAddonMenu() .addItem('Convert to Markdown', 'ConvertToMarkdown') .addToUi(); } /** * Runs when the add-on is installed. * * @param {object} e The event parameter for a simple onInstall trigger. To * determine which authorization mode (ScriptApp.AuthMode) the trigger is * running in, inspect e.authMode. (In practice, onInstall triggers always * run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or * AuthMode.NONE.) */ function onInstall(e) { //When the document is first installed, clear out the user preferences set already var scriptProperties = PropertiesService.getUserProperties(); onOpen(e); } 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 = [];
  try {
    // 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.jekyllHeader==="start" && !inSrc) {
          inSrc=true;
          text+="---\n"
        } else if (result.jekyllHeader==="end" && inSrc) {
          inSrc=false;
          var today = new Date();
          var date = today.getFullYear() + "-" + (today.getMonth() + 1) + "-" + today.getDate()
          // Remove the image dir value
          // text = text.replace(/^\s*{{blogImageDir}}:\s+([a-zA-Z0-9 ]+)/i, "");
          text+="layout: post\ndate: " + date + "\n---\n\n"
        } else if(result.sourceWithType==="start" && !inSrc) {
          inSrc=true;
          text+="```"+result.sourceType+"\n";
        } else if (result.sourceWithType==="end" && inSrc) {
          inSrc=false;
          text+="```\n\n";
        } else 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) { text+=result.text+"\n\n"; } if (result.images && result.images.length>0) { for (var j=0; j 1) { errorMsg = "Error after the line : \"" + sentence[sentence.length - 2] + "\".\n\n" + e; } else if (sentence.length == 1) { errorMsg = "Error after the line : \"" + sentence[sentence.length - 1] + "\".\n\n" + e; } else if (sentence.length == 0) { errorMsg = "Error after the text : \"" + text + "\".\n\n" + e;; } } else { errorMsg = e; } //Showing the error message in alert window. DocumentApp.getUi().alert("Error", errorMsg, DocumentApp.getUi().ButtonSet.OK); } } function escapeHTML(text) { // return text.replace(//g, '>'); return text; } // Process each child element (not just paragraphs). function processParagraph(index, element, inSrc, imageCounter, listCounters) { // First, check for things that require no processing. 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 pOut = ""; 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"); } // Process various types (ElementType). for (var i = 0; i < element.getNumChildren(); i++) { var t=element.getChild(i).getType(); if (t === DocumentApp.ElementType.TABLE_ROW) { // do nothing: already handled TABLE_ROW } else if (t === DocumentApp.ElementType.TEXT) { var txt=element.getChild(i); pOut += txt.getText(); textElements.push(txt); } else if (t === DocumentApp.ElementType.INLINE_IMAGE) { result.images = result.images || []; var contentType = element.getChild(i).getBlob().getContentType(); var extension = ""; if (/\/png$/.test(contentType)) { extension = ".png"; } else if (/\/gif$/.test(contentType)) { extension = ".gif"; } else if (/\/jpe?g$/.test(contentType)) { extension = ".jpg"; } else { throw "Unsupported image type: "+contentType; } var name = imagePrefix + imageCounter + extension; imageCounter++; textElements.push('![]({{ baseurl }}/blog/images/'+ blogImageDir + '/'+name+')'); result.images.push( { "bytes": element.getChild(i).getBlob().getBytes(), "type": contentType, "name": name}); } else if (t === DocumentApp.ElementType.PAGE_BREAK) { // ignore } else if (t === DocumentApp.ElementType.HORIZONTAL_RULE) { textElements.push('* * *\n'); } else if (t === DocumentApp.ElementType.FOOTNOTE) { textElements.push(' (NOTE: '+element.getChild(i).getFootnoteContents().getText()+')'); } else { throw "Paragraph "+index+" of type "+element.getType()+" has an unsupported child: " +t+" "+(element.getChild(i)["getText"] ? element.getChild(i).getText():'')+" index="+index; } } if (textElements.length==0) { // Isn't result empty now? return result; } // evb: Add source pretty too. (And abbreviations: src and srcp.) // process source code block: if (/^\s*blogImageDir:\s+([^ ]+)\s*$/.test(pOut)) { blogImageDir = RegExp.$1; result.text = "" } if(/^\s*---header\s*$/.test(pOut)) { result.jekyllHeader = "start"; } else if (/^\s*```([a-zA-Z0-9]+)\s*$/.test(pOut)) { result.sourceWithType = "start"; result.sourceType = RegExp.$1; } else if (/^\s*---\s+srcp\s*$/.test(pOut) || /^\s*---\s+source pretty\s*$/.test(pOut)) { result.sourcePretty = "start"; } else if (/^\s*---\s+src\s*$/.test(pOut) || /^\s*---\s+source code\s*$/.test(pOut)) { result.source = "start"; } else if (/^\s*---\s+class\s+([^ ]+)\s*$/.test(pOut)) { result.inClass = "start"; result.className = RegExp.$1; } else if (/^\s*```\s*$/.test(pOut)) { result.sourceWithType = "end"; } else if (/^\s*---\s*$/.test(pOut) || /^\s*```\s*$/.test(pOut)) { result.source = "end"; result.sourcePretty = "end"; result.inClass = "end"; result.jekyllHeader = "end" } else if (/^\s*---\s+jsperf\s*([^ ]+)\s*$/.test(pOut)) { result.text = ''; } else { prefix = findPrefix(inSrc, element, listCounters); 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+". "; } } } return prefix; } 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; }