Action

Save to Evernote (Full Formatting)

Posted by akay64, Last update over 3 years ago

Convert draft Markdown to ENML and save to a note in Evernote with the title as note name.

This action uses some scripting to make the draft output be compatible with Evernote (ENML) and preserves the following when saving to Evernote:

  • All heading formatting up to h3 (max supported by new Evernote)
  • Line breaks and makes them Evernote compatable
  • All list indentations
  • Code blocks and make them Evernote compatible
  • Inline code block and formats as monospace font in Evernote
  • Checklists and todo lists with full functionality in Evernote to interact with them
  • All other standard markdown other than Evernote specific stuff.
  • Tables with formatting improvements

Steps

  • script

    const CONFIG_LIST_INDENT_SPACES = 4;
    const CONFIG_MONOSPACE_INLINE_CODEBLOCKS = true;
    const CONFIG_RETAIN_CODE_BLOCK_LANG_AS_COMMENT = true; // Code block langage is not supported by ENML
    
    const DEBUG_STRING = ``;
    const DEBUG = false;
    
    const TYPE_CHECK_UNCHECKED = "TYPE_CHECKLIST_UNCHECKED";
    const TYPE_CHECK_CHECKED = "TYPE_CHECK_CHECKED";
    const TYPE_UL = "TYPE_UL";
    const TYPE_OL = "TYPE_OL";
    
    let mmd;
    
    if (!DEBUG) {
      mmd = MultiMarkdown.create();
    
      mmd.format = "html";
      mmd.noMetadata = true;
      mmd.footnotesEnabled = false;
      mmd.criticMarkup = true;
    }
    
    function getListIndent(listItemType, listItem) {
    
      function indentPosition(RegexTest) {
        const indexLocationToFirstChar = listItem.match(RegexTest);
    
    
        if (indexLocationToFirstChar) {
    
          const indent = indexLocationToFirstChar[2];
          let spacesCount = 0;
    
          if (indent) {
            spacesCount = indent.length
          }
    
          return Math.ceil(spacesCount/CONFIG_LIST_INDENT_SPACES)
        } else {
          return 0
        }
      };
    
      switch(listItemType) {
        case TYPE_CHECK_UNCHECKED:
          return indentPosition(new RegExp(/^((\s+)?- \[\s\])/));
        case TYPE_CHECK_CHECKED:
          return indentPosition(new RegExp(/^((\s+)?- \[x\])/));
        case TYPE_UL:
          return indentPosition(new RegExp(/^((\s+)?-\s)/)) || indentPosition(new RegExp(/^((\s+)?\*\s)/));
        case TYPE_OL:
          return indentPosition(new RegExp(/^((\s+)?\d+\.\s)/));
      }
    }
    
    function getListItemType(line) {
      if (!line) return null;
    
      const trimmedLine = line.trim();
      const firstFiveChars = trimmedLine.substring(0, 5);
      const firstTwoChars = trimmedLine.substring(0, 2);
    
      if (firstFiveChars === "- [ ]" ) {
        return TYPE_CHECK_UNCHECKED;
      } else if(firstFiveChars === "- [x]") {
        return TYPE_CHECK_CHECKED;
      } else if (firstTwoChars === "- " || firstTwoChars === "* ") {
        return TYPE_UL;
      } else if (firstFiveChars.match(/^\d+\.\s/)) {
        return TYPE_OL;
      } else {
        return null
      }
    }
    
    let draftBody = '';
    
    if (DEBUG) {
      draftBody = DEBUG_STRING;
    } else {
      draftBody = draft.content;
    }
    
    // Fix <hr /> converted incorrectly by some apps in correctly by some apps
    draftBody = draftBody
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/^-\s-\s-\s-$/gm, "---")
    .replace(/\t/g,"    ");
    
    const originalContent = draftBody.split("\n");
    const originalContentWorkingCopy = draftBody.split("\n");
    originalContent.shift();
    originalContentWorkingCopy.shift();
    
    let listGroups = [[]];
    let processingGroup = false;
    
    // Create groupings of lists to be converted to ENML
    originalContent.forEach((line, index) => {
    
      const lineRecord = {};
      const listItemType = getListItemType(line);
      let listItemFound = Boolean(listItemType);
    
      if (listItemType) {
        lineRecord.type = listItemType;
        lineRecord.indentPosition = getListIndent(listItemType, line);
      }
    
      const groupStartCriteriaMatch = (!originalContent[index-1] ||
        !Boolean(getListItemType(originalContent[index-1]))) &&
        listItemFound;
      const groupEndCriteriaMatch = (!originalContent[index+1] ||
      originalContent[index+1].trim() === "") || !Boolean(getListItemType(originalContent[index+1]));
    
    	if (processingGroup || groupStartCriteriaMatch) {
        if (!processingGroup) {
          processingGroup = true;
          originalContentWorkingCopy[index] = `{{REPLACE_LIST-${listGroups.length}}}`;
        } else {
          originalContentWorkingCopy[index] = null;
        }
    
        lineRecord.content = line.trim();
    
        listGroupsLastIndex = listGroups.length-1;
        listGroups[listGroupsLastIndex].push(lineRecord);
    
        if (groupEndCriteriaMatch) {
          listGroups.push([]);
        	processingGroup = false;
        }
      }
    });
    
    // Clean up list groups
    listGroups = listGroups.filter((listGroup) => Boolean(listGroup.length));
    
    let processedLists = [];
    
    if (listGroups.length) {
      listGroups.forEach((listGroup, groupIndex) => {
    
        processedLists.push([]);
        let currentIndentPosition = 0;
        let endTagStack = [];
    
        listGroup.forEach((listItem, itemIndex) => {
    
          if (itemIndex === 0) {
            if (listItem.type === TYPE_OL) {
              processedLists[groupIndex].push("<ol>");
              endTagStack.push("</ol>");
            } else {
              processedLists[groupIndex].push("<ul>");
              endTagStack.push("</ul>");
            }
          }
    
          if(currentIndentPosition != listItem.indentPosition) {
            if (currentIndentPosition < listItem.indentPosition) {
              if (listItem.type === TYPE_OL) {
                processedLists[groupIndex].push("<ol>");
                endTagStack.push("</ol>");
              } else {
                processedLists[groupIndex].push("<ul>");
                endTagStack.push("</ul>");
              }
            } else {
              (new Array(currentIndentPosition - listItem.indentPosition)).fill().forEach(() => {
                processedLists[groupIndex].push(endTagStack.pop())
              });
            }
    
            currentIndentPosition = listItem.indentPosition;
          }
    
          // Prepare list item for insertion
          let preparedListItem = listItem.content.trim();
    
    
          function markdownRenderList(content) {
            if (!DEBUG) {
              return mmd.render(content).replace("<p>", "").replace("</p>", "");
            } else {
              return content;
            };
          }
    
          switch (listItem.type) {
            case TYPE_CHECK_UNCHECKED:
              preparedListItem = preparedListItem.replace("- [ ] ", '');
              preparedListItem = markdownRenderList(preparedListItem);
              preparedListItem = '<en-todo checked="false" />' + preparedListItem;
              break;
            case TYPE_CHECK_CHECKED:
              preparedListItem = preparedListItem.replace("- [x] ", '');
              preparedListItem = markdownRenderList(preparedListItem);
              preparedListItem = '<en-todo checked="true" />' + preparedListItem;
              break;
            case TYPE_UL:
              preparedListItem = preparedListItem.replace(/^-\s|^\*\s/, '');
              preparedListItem = markdownRenderList(preparedListItem);
              break;
            case TYPE_OL:
              preparedListItem = preparedListItem.replace(/^\d+\.\s/, '');
              preparedListItem = markdownRenderList(preparedListItem);
              break;
          }
    
          processedLists[groupIndex].push(`<li>${preparedListItem}</li>`);
    
          if(listGroups[groupIndex].length-1 === itemIndex) {
    
            new Array(endTagStack.length).fill().forEach(() => {
              processedLists[groupIndex].push(endTagStack.pop())
            });
          }
        })
      })
    }
    
    // Final result
    
    let reassembledBody = originalContentWorkingCopy
    .filter((line) => line !== null
    ).join("\n");
    
    let markdownOutputBody = '';
    
    if (CONFIG_RETAIN_CODE_BLOCK_LANG_AS_COMMENT) {
      reassembledBody = reassembledBody.replace(/^```([A-Za-z]+)$/mg, (_, c1) => "```\n//" + c1 + "\n")
    } else {
      reassembledBody = reassembledBody.replace(/^```([A-Za-z]+)/mg, "```");
    }
    
    // New Evernote does not support more than 3 heading levels
    reassembledBody = reassembledBody
    .replace(/^####\s/mg, "### ")
    .replace(/^#####\s/mg, "### ")
    .replace(/^######\s/mg, "### ");
    
    if (DEBUG) {
      markdownOutputBody = reassembledBody;
    } else {
      markdownOutputBody = mmd.render(reassembledBody);
    }
    
    processedLists.forEach((list, index) => {
      markdownOutputBody = markdownOutputBody.replace(`{{REPLACE_LIST-${index + 1}}}`, list.join("\n"));
    })
    
    // solution to malformed p multiline
    let pMatcher = /<p>((.|\n)*?)<\/p>/g;
    let pmarkdownOutputBodyCopy = markdownOutputBody;
    
    while (matched = pMatcher.exec(pmarkdownOutputBodyCopy)) {
      const splitedLines = matched[1].split("\n");
    
      if (
          splitedLines.length > 1
          && !matched[0].includes("<ul>")
          && !matched[0].includes("<ol>")
      	 ) {
        const output = splitedLines.map((split) => {
          return `<div>${split}</div>`;
        });
    
        markdownOutputBody = markdownOutputBody.replace(matched[0], output.join("\n"))
      }
    }
    
    // Improve table formatting
    
    markdownOutputBody = markdownOutputBody
    .replace(/\n<table>((.|\n)*?)<thead>/g, "<table><thead>");
    
    // Take code blocks out of base flow before formatting line breaks
    let preCodeMatcher = /<pre><code>((.|\n)*?)<\/code><\/pre>/g;
    let preCodeMarkdownOutputBodyCopy = markdownOutputBody;
    
    while (matched = preCodeMatcher.exec(preCodeMarkdownOutputBodyCopy)) {
      markdownOutputBody = markdownOutputBody.replace(matched[0],
        '<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><pre>'
        +(matched[1].split("\n").map((codeLine, index, content) => {
          if (index === (content.length-1)) {
           return '';
          }
          if (codeLine.trim() === '') {
            return `<div><br /></div>`;
          }
          return `<div>${codeLine}</div>`;
        })).join("")
        +"</pre></div>"
        );
    }
    
    // ENML compatable line breaks
    let enmlOutput = markdownOutputBody
    .replace(/<figure>/g, '')
    .replace(/<\/figure>/g, '')
    .replace(/<figcaption>/g,'')
    .replace(/<\/figcaption>/g, '')
    .replace(/<p>/g, "<div>")
    .replace(/<\/p>/g, "</div>")
    .replace(/\n(\s)*?\n/g,"\n<div><br \/><\/div>\n");
    
    if (CONFIG_MONOSPACE_INLINE_CODEBLOCKS) {
      enmlOutput = enmlOutput.replace(/<code>(.*?)<\/code>/g,
      (_, c1) => `<span style="--en-fontfamily: monospace; font-family: &quot;Source Code Pro&quot;,monospace">\`${c1}\`</span>`)
    }
    
    if (DEBUG) {
      console.log(enmlOutput);
    } else {
      draft.setTemplateTag("enmlOutput", enmlOutput);
    }
  • evernote

    nameTemplate
    [[safe_title]]
    notebookTemplate
    tagTemplate
    [[tags]]
    template
    [[enmlOutput]]
    format
    enml
    writeType
    create

Options

  • After Success Default
    Notification Info
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.