From 7b853953bfa2dd0159dbb093d4e37352755b7f0c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 24 Dec 2024 14:17:46 -0500 Subject: [PATCH] Export comments Fixes #20 --- addon/appsscript.json | 13 +- addon/comments.gs | 86 ++ addon/gdc.gs | 1258 +++++++++++++++------------ addon/html-comment-converter.gs | 62 ++ addon/html.gs | 393 +++++---- addon/markdown-comment-converter.gs | 34 + addon/sidebar.html | 5 + 7 files changed, 1108 insertions(+), 743 deletions(-) create mode 100644 addon/comments.gs create mode 100644 addon/html-comment-converter.gs create mode 100644 addon/markdown-comment-converter.gs diff --git a/addon/appsscript.json b/addon/appsscript.json index 4ae5cd7..b6fa207 100644 --- a/addon/appsscript.json +++ b/addon/appsscript.json @@ -1,10 +1,17 @@ { "timeZone": "America/Los_Angeles", - "dependencies": {}, + "dependencies": { + "enabledAdvancedServices": [{ + "userSymbol": "Drive", + "serviceId": "drive", + "version": "v2" + }] + }, "oauthScopes": [ "https://www.googleapis.com/auth/documents.currentonly", - "https://www.googleapis.com/auth/script.container.ui" + "https://www.googleapis.com/auth/script.container.ui", + "https://www.googleapis.com/auth/drive.readonly" ], "exceptionLogging": "STACKDRIVER", "runtimeVersion": "DEPRECATED_ES5" -} +} \ No newline at end of file diff --git a/addon/comments.gs b/addon/comments.gs new file mode 100644 index 0000000..3e4b407 --- /dev/null +++ b/addon/comments.gs @@ -0,0 +1,86 @@ +// Functions for handling comments from Drive API + +function processCommentReplies(reply) { + return { + content: reply.content, + author: reply.author.displayName, + created: reply.createdTime, + }; +} + +function getDocumentComments() { + const doc = DocumentApp.getActiveDocument(); + const docId = doc.getId(); + + try { + const commentsResponse = Drive.Comments.list(docId, { + maxResults: 100, + fields: "*", + }); + + if (!commentsResponse.comments) return []; + + return commentsResponse.comments.map((comment) => ({ + id: comment.id, + content: cleanHtmlContent(comment.content), + author: comment.author.displayName, + created: comment.createdTime, + quotedText: comment.quotedFileContent + ? cleanHtmlContent(comment.quotedFileContent.value) + : null, + anchor: comment.anchor, + replies: (comment.replies || []).map(processCommentReplies), + })); + } catch (e) { + Logger.log("Error getting comments:", e.toString()); + return []; + } +} + +// This helper formats comments for Markdown output +function formatMarkdownComment(comment, referenceId) { + let output = `[${referenceId}] **${comment.author}** (${new Date( + comment.created + ).toLocaleString()}): ${comment.content}\n`; + + if (comment.replies && comment.replies.length > 0) { + comment.replies.forEach((reply) => { + output += ` > **${reply.author}** (${new Date( + reply.created + ).toLocaleString()}): ${reply.content}\n`; + }); + } + + return output; +} + +// This helper formats comments for HTML output +function formatHtmlComment(comment, referenceId) { + let output = `
\n`; + output += `

${comment.author} (${new Date( + comment.created + ).toLocaleString()}): ${comment.content}

\n`; + + if (comment.replies && comment.replies.length > 0) { + comment.replies.forEach((reply) => { + output += `

${reply.author} (${new Date( + reply.created + ).toLocaleString()}): ${reply.content}

\n`; + }); + } + + output += `
\n`; + return output; +} + +// Clean HTML content in comment text +function cleanHtmlContent(content) { + if (!content) return ""; + return content + .replace(/<[^>]*>/g, "") // Remove HTML tags + .replace(/"/g, '"') // Fix quotes + .replace(/'/g, "'") // Fix apostrophes + .replace(/&/g, "&") // Fix ampersands + .replace(/\s+/g, " ") // Normalize whitespace + .trim(); +} diff --git a/addon/gdc.gs b/addon/gdc.gs index 9d6e706..8886ec1 100644 --- a/addon/gdc.gs +++ b/addon/gdc.gs @@ -43,7 +43,7 @@ var gdc = gdc || {}; // NOTE: Check these before publishing! (and remove β if appropriate) // Use banner comment (very rarely) to communicate important information // (like recent bugs affecting output) at the top of the conversion test. -gdc.banner = ''; // This is the general case. +gdc.banner = ""; // This is the general case. /* gdc.banner = '\n\n' + gdc.info; + gdc.info = "\n\n" + gdc.info; } // Assemble output. @@ -1751,28 +1825,40 @@ md.doMarkdown = function(config) { gdc.out = gdc.alertMessage + gdc.out; // Add info comment if desired. if (!gdc.suppressInfo) { - gdc.out = gdc.info + '\n----->\n\n' + gdc.out; - } else if (gdc.suppressInfo && gdc.errorSummary !== '') { + gdc.out = gdc.info + "\n----->\n\n" + gdc.out; + } else if (gdc.suppressInfo && gdc.errorSummary !== "") { // But notify if there are errors. - gdc.out = '\n' + gdc.out; + gdc.out = "\n" + gdc.out; } // Always include the banner. gdc.out = gdc.banner + gdc.out; - + + // Add comment support (if enabled) + const comments = getDocumentComments(); + if (comments && comments.length > 0) { + if (gdc.docType === gdc.docTypes.md) { + gdc.out = insertMarkdownCommentReferences(gdc.out, comments); + gdc.out += createMarkdownCommentSection(comments); + } else { + gdc.out = insertHtmlCommentReferences(gdc.out, comments); + gdc.out = + addCommentStyles() + gdc.out + createHtmlCommentSection(comments); + } + } + return gdc.out; }; // Switch for handling different child elements for Markdown conversion. // Use for all element types, unless they have no children. // See ElementType enum for actual type names. -md.handleChildElement = function(child) { - +md.handleChildElement = function (child) { var childType = child.getType(); // Get indent if possible for this element. But we do not want to // change indent if it's an empty paragraph. - if (child.getIndentStart && child.getNumChildren() ) { + if (child.getIndentStart && child.getNumChildren()) { gdc.indent = child.getIndentStart(); } @@ -1787,8 +1873,13 @@ md.handleChildElement = function(child) { case TEXT: try { gdc.handleText(child); - } catch(e) { - gdc.log('\nERROR handling text element:\n\n' + e + '\n\nText: ' + child.getText()); + } catch (e) { + gdc.log( + "\nERROR handling text element:\n\n" + + e + + "\n\nText: " + + child.getText() + ); } break; case LIST_ITEM: @@ -1826,7 +1917,7 @@ md.handleChildElement = function(child) { break; case FOOTER_SECTION: case HEADER_SECTION: - gdc.warn('Not processing header or footer sections.'); + gdc.warn("Not processing header or footer sections."); break; case INLINE_DRAWING: gdc.handleInlineDrawing(); @@ -1834,10 +1925,10 @@ md.handleChildElement = function(child) { case INLINE_IMAGE: try { gdc.handleImage(child); - } catch(e) { + } catch (e) { gdc.errorCount++; - gdc.log('\nERROR while handling inline image:\n' + e); - gdc.alert('error handling inline image'); + gdc.log("\nERROR while handling inline image:\n" + e); + gdc.alert("error handling inline image"); } break; case PAGE_BREAK: @@ -1847,16 +1938,15 @@ md.handleChildElement = function(child) { gdc.handleEquation(); break; case UNSUPPORTED: - gdc.log('child element: UNSUPPORTED'); + gdc.log("child element: UNSUPPORTED"); break; default: - gdc.log('child element: unknown'); - }; + gdc.log("child element: unknown"); + } }; // Handles Markdown and HTML paragraphs. -md.handleParagraph = function(para) { - +md.handleParagraph = function (para) { // When we're entering a paragraph, close any open HTML lists. // Not perfect, but better than leaving them hanging in HTML. // Markdown actually works better here. @@ -1871,9 +1961,9 @@ md.handleParagraph = function(para) { if (gdc.numChildren === 0) { if (gdc.inCodeBlock) { // Preserve newlines in code block (or single-cell table code block). - if ( gdc.isCodeLine(para.getNextSibling()) || gdc.isSingleCellTable) { + if (gdc.isCodeLine(para.getNextSibling()) || gdc.isSingleCellTable) { // Write a placeholder for newline: will replace after wrapping. - gdc.writeStringToBuffer(''); + gdc.writeStringToBuffer(""); } } return; @@ -1896,7 +1986,7 @@ md.handleParagraph = function(para) { // Check here to see if the first line signals language for the code block. var lang = gdc.getLang(para.getText()); - if (lang !== '') { + if (lang !== "") { gdc.startCodeBlock(lang); return; // Do not want the ``` line in the code block. } else { @@ -1917,13 +2007,12 @@ md.handleParagraph = function(para) { gdc.handleDdef(para); } else if (gdc.inDlist) { // Close definition list if we're in one now. (Make this a function.) - gdc.closeDlist(); + gdc.closeDlist(); } -// Headings, codeblocks, regular para. May need to check for codeblock earlier. + // Headings, codeblocks, regular para. May need to check for codeblock earlier. var heading = para.getHeading(); if (heading !== DocumentApp.ParagraphHeading.NORMAL) { - // Don't process headings that just contain whitespace. if (gdc.isWhitespacePara(para)) { return; @@ -1943,19 +2032,27 @@ md.handleParagraph = function(para) { // do nothing: we also want to not add the tablePrefix. // But check for table cell or definition list. } else if (!gdc.startingTableCell && !gdc.inDlist) { - // This is where we want to check for right/center alignment so that the proper paragraph style can be applied. - if (gdc.isHTML && para.getAlignment() === DocumentApp.HorizontalAlignment.RIGHT && para.isLeftToRight()) { + // This is where we want to check for right/center alignment so that the proper paragraph style can be applied. + if ( + gdc.isHTML && + para.getAlignment() === DocumentApp.HorizontalAlignment.RIGHT && + para.isLeftToRight() + ) { gdc.writeStringToBuffer('\n

\n'); // Not sure what this does? gdc.useHtml(); // TODO: check this! //gdc.isRightAligned = true; // TODO: check this! - } else if (gdc.isHTML && para.getAlignment() === DocumentApp.HorizontalAlignment.CENTER && para.isLeftToRight()) { + } else if ( + gdc.isHTML && + para.getAlignment() === DocumentApp.HorizontalAlignment.CENTER && + para.isLeftToRight() + ) { gdc.writeStringToBuffer('\n

\n'); gdc.useHtml(); } else { gdc.writeStringToBuffer(gdc.markup.pOpen); - } - } + } + } // We want paragraphs after the first text in a table cell. gdc.startingTableCell = false; @@ -1964,15 +2061,15 @@ md.handleParagraph = function(para) { if (gdc.isFootnote) { gdc.writeStringToBuffer(gdc.footnoteIndent); } - + // Check for indent. if (gdc.indent) { - var n = gdc.indent/36; + var n = gdc.indent / 36; // Note: code block code already accounts for indent. if (!gdc.inCodeBlock) { - gdc.writeStringToBuffer('\n'); - for (i = 0; i < n; i++ ) { - gdc.writeStringToBuffer(' '); + gdc.writeStringToBuffer("\n"); + for (i = 0; i < n; i++) { + gdc.writeStringToBuffer(" "); } } } @@ -1995,7 +2092,7 @@ md.handleParagraph = function(para) { // Is this necessary? if (gdc.isRightAligned) { - gdc.writeStringToBuffer('

\n'); + gdc.writeStringToBuffer("

\n"); gdc.isRightAligned = false; } @@ -2005,7 +2102,7 @@ md.handleParagraph = function(para) { // NOTE: This doesn't deal with duplicate headings (yet). var id = gdc.headingIds[para.getText().trim()]; if (id && !gdc.htmlHeadings) { - gdc.writeStringToBuffer(' {#' + id + '}'); + gdc.writeStringToBuffer(" {#" + id + "}"); } } if (html.isHeading) { @@ -2015,7 +2112,7 @@ md.handleParagraph = function(para) { // Close
,
if (gdc.inDdef) { - gdc.writeStringToBuffer(gdc.markup.ddClose + '\n'); + gdc.writeStringToBuffer(gdc.markup.ddClose + "\n"); gdc.inDdef = false; gdc.gotDdef = true; } else if (gdc.inDterm) { @@ -2031,57 +2128,62 @@ md.handleParagraph = function(para) { } } - // We also want to check here. + // We also want to check here. gdc.maybeCloseList(para); }; // end md.handleParagraph // Handle the heading type of the paragraph. Fall through for NORMAL. // This nice bit is from Renato Mangini's original gdocs2md // (though it's gotten a bit more complex with the demotion check). -md.handleHeading = function(heading) { - var buf = ''; +md.handleHeading = function (heading) { + var buf = ""; // Add an extra newline before a heading. - buf += ''; + buf += ""; if (gdc.demoteHeadings) { - buf += '#'; + buf += "#"; } // Count H1s to warn for too many. - if (heading === DocumentApp.ParagraphHeading.HEADING1 && !gdc.demoteHeadings) { + if ( + heading === DocumentApp.ParagraphHeading.HEADING1 && + !gdc.demoteHeadings + ) { gdc.h1Count++; } switch (heading) { // Add a # for each heading level. Fall through to accumulate the right number of #s. case DocumentApp.ParagraphHeading.HEADING6: - // Do not demote h6, but warn and mark them. - var warning = 'H6 not demoted to H7.'; - if (gdc.demoteHeadings) { - if (!gdc.warnedAboutH7) { - gdc.warn(warning + ' Look for "' + warning + '" inline.'); - gdc.warnedAboutH7 = true; - } - gdc.writeStringToBuffer('\n\n'); - } else { - buf += '#'; - } - case DocumentApp.ParagraphHeading.HEADING5: buf += '#'; - case DocumentApp.ParagraphHeading.HEADING4: buf += '#'; - case DocumentApp.ParagraphHeading.HEADING3: buf += '#'; + // Do not demote h6, but warn and mark them. + var warning = "H6 not demoted to H7."; + if (gdc.demoteHeadings) { + if (!gdc.warnedAboutH7) { + gdc.warn(warning + ' Look for "' + warning + '" inline.'); + gdc.warnedAboutH7 = true; + } + gdc.writeStringToBuffer("\n\n"); + } else { + buf += "#"; + } + case DocumentApp.ParagraphHeading.HEADING5: + buf += "#"; + case DocumentApp.ParagraphHeading.HEADING4: + buf += "#"; + case DocumentApp.ParagraphHeading.HEADING3: + buf += "#"; case DocumentApp.ParagraphHeading.HEADING2: case DocumentApp.ParagraphHeading.SUBTITLE: - buf += '#'; + buf += "#"; case DocumentApp.ParagraphHeading.HEADING1: case DocumentApp.ParagraphHeading.TITLE: - buf += '# '; + buf += "# "; default: } gdc.writeStringToBuffer(buf); }; -md.handleListItem = function(listItem) { - +md.handleListItem = function (listItem) { // Save indent for things inside this list. gdc.indent = listItem.getIndentStart(); @@ -2093,16 +2195,16 @@ md.handleListItem = function(listItem) { // Go through children of this list item (same elements as paragraph). // Preliminaries: do the list prefix. - var prefix = '\n', - glyphType = listItem.getGlyphType(), - nestLevel = listItem.getNestingLevel(), - isList = true; + var prefix = "\n", + glyphType = listItem.getGlyphType(), + nestLevel = listItem.getNestingLevel(), + isList = true; gdc.nestLevel = nestLevel; // Open list if necessary. if (!gdc.isList) { gdc.isList = true; - gdc.writeStringToBuffer('\n'); + gdc.writeStringToBuffer("\n"); if (gdc.isBullet(glyphType)) { prefix += gdc.markup.ulOpen; } else { @@ -2110,31 +2212,34 @@ md.handleListItem = function(listItem) { } } - if (gdc.isFootnote) { prefix += gdc.footnoteIndent; } + if (gdc.isFootnote) { + prefix += gdc.footnoteIndent; + } md.maybeEndCodeBlock(); // Indent the list properly. for (var i = 0; i < nestLevel; i++) { // Markdown requires 4 spaces to separate nesting levels. - prefix += ''; + prefix += ""; } // Check for bullet list. - if (gdc.isBullet(glyphType) === 'bullet') { + if (gdc.isBullet(glyphType) === "bullet") { prefix += gdc.markup.ulItem; - } else if (gdc.isBullet(glyphType) === 'ordered list') { + } else if (gdc.isBullet(glyphType) === "ordered list") { // Ordered list. - var key = listItem.getListId() + '.' + nestLevel; + var key = listItem.getListId() + "." + nestLevel; // Initialize list counter. var counter = gdc.listCounters[key] || 0; counter++; gdc.listCounters[key] = counter; // Increment ordered list counter. - prefix += counter + '. '; + prefix += counter + ". "; // Alternative is to use 1. for all ordered list items, but less readable. // prefix += gdc.markup.olItem; - } else if (gdc.isBullet(glyphType) === 'checkbox') { // Maybe it's unecessary to spell out and just leave as an "else" statement. - prefix += gdc.markup.cboxItem + } else if (gdc.isBullet(glyphType) === "checkbox") { + // Maybe it's unecessary to spell out and just leave as an "else" statement. + prefix += gdc.markup.cboxItem; } gdc.writeStringToBuffer(prefix); @@ -2147,44 +2252,46 @@ md.handleListItem = function(listItem) { }; // Switch Markdown table-handling when Markdown tables are sane. -md.handleTable = function(tableElement) { +md.handleTable = function (tableElement) { // For now, we'll do tables in HTML. gdc.isTable = true; gdc.useHtml(); // Go through children of this table. - gdc.writeStringToBuffer('\n\n'); + gdc.writeStringToBuffer("\n\n
"); md.childLoop(tableElement); - gdc.writeStringToBuffer('\n
\n\n'); + gdc.writeStringToBuffer("\n\n\n"); gdc.isTable = false; gdc.useMarkdown(); }; -md.handleTableRow = function(tableRowElement) { +md.handleTableRow = function (tableRowElement) { // Go through children of this table. - gdc.writeStringToBuffer('\n '); + gdc.writeStringToBuffer("\n "); md.childLoop(tableRowElement); - gdc.writeStringToBuffer('\n '); + gdc.writeStringToBuffer("\n "); }; -md.handleTableCell = function(tableCellElement) { +md.handleTableCell = function (tableCellElement) { // Go through children of this table. - gdc.writeStringToBuffer('\n '); + gdc.writeStringToBuffer("\n "); md.childLoop(tableCellElement); md.maybeEndCodeBlock(); - gdc.writeStringToBuffer('\n '); + gdc.writeStringToBuffer("\n "); }; // Handles elements that can have children // (by passing each one off to the the handleChildElement() dispatcher). -md.childLoop = function(element) { - if (!element) { return; } +md.childLoop = function (element) { + if (!element) { + return; + } for (var i = 0, z = element.getNumChildren(); i < z; i++) { md.handleChildElement(element.getChild(i)); } }; // Formats footnotes for Markdown. -md.handleFootnote = function(footnote) { +md.handleFootnote = function (footnote) { gdc.footnoteNumber++; var fSection = footnote.getFootnoteContents(); if (!fSection) { @@ -2194,15 +2301,16 @@ md.handleFootnote = function(footnote) { } // Write footnote number in the body text. - gdc.writeStringToBuffer('[^' + gdc.footnoteNumber + ']'); + gdc.writeStringToBuffer("[^" + gdc.footnoteNumber + "]"); // Now, we're ready for the footnote itself. gdc.isFootnote = true; if (gdc.footnoteNumber === 1) { - gdc.writeStringToBuffer('\n\n' - + '\n## Notes'); + gdc.writeStringToBuffer( + "\n\n" + "\n## Notes" + ); } - gdc.writeStringToBuffer('\n\n[^' + gdc.footnoteNumber + ']:'); + gdc.writeStringToBuffer("\n\n[^" + gdc.footnoteNumber + "]:"); md.childLoop(fSection); gdc.isFootnote = false; }; @@ -2210,10 +2318,10 @@ md.handleFootnote = function(footnote) { // Starts a code block (either Markdown-fenced or HTML). // Default lang is defined earlier (either 'none' or ''), // which still results in prettyprint guessing for HTML. -gdc.startCodeBlock = function(lang) { +gdc.startCodeBlock = function (lang) { gdc.inCodeBlock = true; - gdc.codeIndent = ''; - gdc.fourSpaces = ' '; + gdc.codeIndent = ""; + gdc.fourSpaces = " "; // Write the newlines first. gdc.writeStringToBuffer(md.preCodeBlock); @@ -2221,22 +2329,26 @@ gdc.startCodeBlock = function(lang) { // Account for indent if inside a list. // Add indent before opening the code block. if (gdc.isList && gdc.indent > 0) { - var indent = gdc.indent/36; // 36 pixels per indent level for Docs. + var indent = gdc.indent / 36; // 36 pixels per indent level for Docs. for (var i = 0; i < indent; i++) { gdc.codeIndent += gdc.fourSpaces; } gdc.writeStringToBuffer(gdc.codeIndent); } - if (gdc.docType === gdc.docTypes.md - && !(gdc.isTable && gdc.isSingleCellTable) ) { - gdc.writeStringToBuffer(md.openCodeBlock + lang + ''); + if ( + gdc.docType === gdc.docTypes.md && + !(gdc.isTable && gdc.isSingleCellTable) + ) { + gdc.writeStringToBuffer(md.openCodeBlock + lang + ""); } else { // For HTML, do not add lang if empty, but do not prettyprint if lang is 'none'. - if (lang === 'none') { + if (lang === "none") { gdc.writeStringToBuffer(html.openCodeBlockLangNone); - } else if (lang !== '') { - gdc.writeStringToBuffer(html.openCodeBlockStart + lang + html.openCodeBlockEnd); + } else if (lang !== "") { + gdc.writeStringToBuffer( + html.openCodeBlockStart + lang + html.openCodeBlockEnd + ); } else { gdc.writeStringToBuffer(html.openCodeBlock); } @@ -2244,7 +2356,7 @@ gdc.startCodeBlock = function(lang) { }; // Ends the code block only if we're already in a code block! -md.maybeEndCodeBlock = function() { +md.maybeEndCodeBlock = function () { // Do not end code block if we're in a single-cell table. if (gdc.isSingleCellTable === true) { return; diff --git a/addon/html-comment-converter.gs b/addon/html-comment-converter.gs new file mode 100644 index 0000000..f96e9a1 --- /dev/null +++ b/addon/html-comment-converter.gs @@ -0,0 +1,62 @@ +// HTML-specific comment handling code + +// Helper function to insert comment references in text +function insertHtmlCommentReferences(text, comments) { + let output = text; + let commentCounter = 1; + + comments.forEach((comment) => { + if (comment.quotedText && output.includes(comment.quotedText)) { + const reference = `[${commentCounter}]`; + output = output.replace( + comment.quotedText, + `${comment.quotedText}${reference}` + ); + commentCounter++; + } + }); + + return output; +} + +// Create HTML comment section +function createHtmlCommentSection(comments) { + if (!comments || comments.length === 0) return ""; + + let output = '\n

Comments

\n
\n'; + let commentCounter = 1; + + comments.forEach((comment) => { + output += formatHtmlComment(comment, commentCounter++); + }); + + output += "
\n"; + return output; +} + +// Add necessary CSS for comment styling +function addCommentStyles() { + return ` + +`; +} diff --git a/addon/html.gs b/addon/html.gs index 67ac386..b019388 100644 --- a/addon/html.gs +++ b/addon/html.gs @@ -22,38 +22,37 @@ var html = html || { // Attribute change markers. - codeOpen: '', - codeClose: '', - italicOpen: '', - italicClose:'', - boldOpen: '', - boldClose: '', - + codeOpen: "", + codeClose: "", + italicOpen: "", + italicClose: "", + boldOpen: "", + boldClose: "", + // HTML code blocks (do not add \n at end of
):
-  openCodeBlock:         '\n\n
',
-  openCodeBlockStart:    '\n\n
',
-  openCodeBlockLangNone: '\n\n
',
-  closeCodeBlock:        '
\n\n', + openCodeBlock: '\n\n
',
+  openCodeBlockStart: '\n\n
',
+  openCodeBlockLangNone: "\n\n
",
+  closeCodeBlock: "
\n\n", // non-semantic underline, since Docs supports it. underlineStart: '', - underlineEnd: '', - + underlineEnd: "", - // I think we need to track these independently because the previous/next sibling won't always be a list item, - // thus not giving reliable nesting level. + // I think we need to track these independently because the previous/next sibling won't always be a list item, + // thus not giving reliable nesting level. listNestingLevel: 0, - // This will also help us know if a list item needs to be closed before opening a new one. - inListItem: false + // This will also help us know if a list item needs to be closed before opening a new one. + inListItem: false, }; -html.tablePrefix = ' '; +html.tablePrefix = " "; -html.doHtml = function(config) { +html.doHtml = function (config) { // Basically, we can use the same code as doMarkdown, just change the markup. gdc.useHtml(); - + gdc.config(config); // Get the body elements. var elements = gdc.getElements(); @@ -69,30 +68,51 @@ html.doHtml = function(config) { izip.createImagesZip(); } } - + if (gdc.hasImages) { - gdc.info += '\n* This document has images: check for ' + gdc.alertPrefix; - gdc.info += ' inline image link in generated source and store images to your server.'; - gdc.info += ' NOTE: Images in exported zip file from Google Docs may not appear in '; - gdc.info += ' the same order as they do in your doc. Please check the images!\n'; + gdc.info += "\n* This document has images: check for " + gdc.alertPrefix; + gdc.info += + " inline image link in generated source and store images to your server."; + gdc.info += + " NOTE: Images in exported zip file from Google Docs may not appear in "; + gdc.info += + " the same order as they do in your doc. Please check the images!\n"; } - + if (gdc.hasFootnotes) { - gdc.info += '\n* Footnote support in HTML is alpha: please check your footnotes.'; + gdc.info += + "\n* Footnote support in HTML is alpha: please check your footnotes."; + } + + // Add comment support (if enabled) + const comments = getDocumentComments(); + if (comments && comments.length > 0) { + if (gdc.docType === gdc.docTypes.md) { + gdc.out = insertMarkdownCommentReferences(gdc.out, comments); + gdc.out += createMarkdownCommentSection(comments); + } else { + gdc.out = insertHtmlCommentReferences(gdc.out, comments); + gdc.out = + addCommentStyles() + gdc.out + createHtmlCommentSection(comments); + } } - + // Record elapsed time. - var eTime = (new Date().getTime() - gdc.startTime)/1000; - gdc.info = '\n\nConversion time: ' + eTime + ' seconds.\n' + gdc.info; + var eTime = (new Date().getTime() - gdc.startTime) / 1000; + gdc.info = "\n\nConversion time: " + eTime + " seconds.\n" + gdc.info; // Note ERRORs or WARNINGs or ALERTs at the top if there are any. - gdc.errorSummary = ''; - if ( gdc.errorCount || gdc.warningCount || gdc.alertCount ) { - gdc.errorSummary = 'You have some errors, warnings, or alerts. ' - + 'If you are using reckless mode, turn it off to see inline alerts.' - + '\n* ERRORs: ' + gdc.errorCount - + '\n* WARNINGs: ' + gdc.warningCount - + '\n* ALERTS: ' + gdc.alertCount; + gdc.errorSummary = ""; + if (gdc.errorCount || gdc.warningCount || gdc.alertCount) { + gdc.errorSummary = + "You have some errors, warnings, or alerts. " + + "If you are using reckless mode, turn it off to see inline alerts." + + "\n* ERRORs: " + + gdc.errorCount + + "\n* WARNINGs: " + + gdc.warningCount + + "\n* ALERTS: " + + gdc.alertCount; } gdc.info = gdc.errorSummary + gdc.info; @@ -101,7 +121,7 @@ html.doHtml = function(config) { // Warn at the top if DEBUG is true. if (DEBUG) { - gdc.info = '\n\n' + gdc.info; + gdc.info = "\n\n" + gdc.info; } // Add info and alert message to top of output. @@ -109,23 +129,22 @@ html.doHtml = function(config) { gdc.out = gdc.alertMessage + gdc.out; // Add info comment if desired. if (!gdc.suppressInfo) { - gdc.out = gdc.info + '\n----->\n\n' + gdc.out; - } else if (gdc.suppressInfo && gdc.errorSummary !== '') { + gdc.out = gdc.info + "\n----->\n\n" + gdc.out; + } else if (gdc.suppressInfo && gdc.errorSummary !== "") { // But notify if there are errors. - gdc.out = '\n' + gdc.out; + gdc.out = "\n" + gdc.out; } // Always include the banner. gdc.out = gdc.banner + gdc.out; - // Output content. gdc.flushBuffer(); gdc.flushFootnoteBuffer(); // Close footnotes list if necessary. if (gdc.hasFootnotes) { - gdc.writeStringToBuffer('\n'); + gdc.writeStringToBuffer("\n"); gdc.flushBuffer(); } @@ -134,16 +153,16 @@ html.doHtml = function(config) { // Switch for handling different child elements for HTML conversion. // Use for all element types, unless they have no children. -html.handleChildElement = function(child) { +html.handleChildElement = function (child) { gdc.useHtml(); var childType = child.getType(); - + // Get indent if possible for this element. // For HTML, we can also count blank paragraphs: Note difference in md.handleChildElement. if (child.getIndentStart) { gdc.indent = child.getIndentStart(); } - + html.checkList(); // Most common element types first. @@ -155,8 +174,13 @@ html.handleChildElement = function(child) { case TEXT: try { gdc.handleText(child); - } catch(e) { - gdc.log('ERROR handling text element:\n\n' + e + '\n\nText: ' + child.getText()); + } catch (e) { + gdc.log( + "ERROR handling text element:\n\n" + + e + + "\n\nText: " + + child.getText() + ); } break; case LIST_ITEM: @@ -199,21 +223,21 @@ html.handleChildElement = function(child) { case EQUATION: break; case UNSUPPORTED: - gdc.log('child element: UNSUPPORTED'); + gdc.log("child element: UNSUPPORTED"); break; default: - gdc.log('child element: unknown'); - }; + gdc.log("child element: unknown"); + } gdc.lastChildType = childType; }; -html.handleTable = function(tableElement) { +html.handleTable = function (tableElement) { // Note that we're converting all tables to HTML. if (!gdc.hasTables) { - gdc.info += '\n* Tables are currently converted to HTML tables.'; + gdc.info += "\n* Tables are currently converted to HTML tables."; gdc.hasTables = true; } - + // init counters. gdc.nCols = 0; gdc.nRows = 0; @@ -228,7 +252,6 @@ html.handleTable = function(tableElement) { // Handle single-cell table here. if (gdc.nCols === 1 && gdc.nRows === 1) { - // Examine text length and text font to see if it's a suspicious single-cell table. var text = tableElement.getChild(0).getText(); var textElement = tableElement.getChild(0).editAsText(); @@ -236,43 +259,46 @@ html.handleTable = function(tableElement) { // But if it's in code font (first and last lines), we'll let it go. // How long is suspiciously long? var singleCellMaxChars = 5120; - if (text.length > singleCellMaxChars && !gdc.textIsCode(textElement) ) { + if (text.length > singleCellMaxChars && !gdc.textIsCode(textElement)) { gdc.warningCount++; - gdc.info += '\nWARNING:\nFound a long single-cell table '; - gdc.info += '(' + text.length + ' characters) starting with:\n'; - gdc.info += '**start sample:**\n'; - gdc.info += text.substring(0, 32) + '\n**end sample**\n'; - gdc.info += 'Check to make sure this is supposed to be a code block.\n'; - gdc.alert('Long single-cell table. Check to make sure this is meant to be a code block.'); + gdc.info += "\nWARNING:\nFound a long single-cell table "; + gdc.info += "(" + text.length + " characters) starting with:\n"; + gdc.info += "**start sample:**\n"; + gdc.info += text.substring(0, 32) + "\n**end sample**\n"; + gdc.info += "Check to make sure this is supposed to be a code block.\n"; + gdc.alert( + "Long single-cell table. Check to make sure this is meant to be a code block." + ); } - + // Markdown or HTML table for single-cell table. // Also handling lang for single-cell table. if (gdc.docType === gdc.docTypes.md && !gdc.isHTML) { gdc.inCodeBlock = gdc.isSingleCellTable = true; var text = tableElement.getText(); - var lang = gdc.getLang(text.split('\n')[0]); - if (lang !== '') { + var lang = gdc.getLang(text.split("\n")[0]); + if (lang !== "") { // Skip first line, since it just specified lang. // XXX: note difference between HTML and Markdown. Why? - text = text.substring(text.indexOf('\n') + 1); - } - + text = text.substring(text.indexOf("\n") + 1); + } + // Write the code block. gdc.startCodeBlock(lang); gdc.writeStringToBuffer(text); - gdc.writeStringToBuffer('' + md.closeCodeBlock); + gdc.writeStringToBuffer("" + md.closeCodeBlock); gdc.inCodeBlock = gdc.isSingleCellTable = false; - } else { // HTML code block. + } else { + // HTML code block. gdc.inCodeBlock = gdc.isSingleCellTable = true; var text = tableElement.getText(); - var lang = gdc.getLang(text.split('\n')[0]); - if (lang !== '') { + var lang = gdc.getLang(text.split("\n")[0]); + if (lang !== "") { // Skip first line, since it just specified lang. // Note difference between HTML and Markdown. Why? - text = text.substring(text.indexOf('\n')); + text = text.substring(text.indexOf("\n")); } - + // Write the code block. gdc.startCodeBlock(lang); text = html.escapeOpenTag(text); @@ -280,43 +306,42 @@ html.handleTable = function(tableElement) { text = util.markSpecial(text); text = util.markNewlines(text); gdc.writeStringToBuffer(text); - gdc.writeStringToBuffer(html.closeCodeBlock); + gdc.writeStringToBuffer(html.closeCodeBlock); gdc.inCodeBlock = gdc.isSingleCellTable = false; } - + return; } - + // Regular table processing. gdc.isTable = true; - + gdc.useHtml(); - + // Go through children of this table. - gdc.writeStringToBuffer('\n\n'); + gdc.writeStringToBuffer("\n\n
"); md.childLoop(tableElement); - gdc.writeStringToBuffer('\n
\n\n'); + gdc.writeStringToBuffer("\n\n\n"); // Turn off guard for table cell. gdc.startingTableCell = false; - + gdc.isTable = false; gdc.useMarkdown(); }; -html.handleTableRow = function(tableRowElement) { - +html.handleTableRow = function (tableRowElement) { if (gdc.isSingleCellTable === true) { md.childLoop(tableRowElement); return; } - + // Go through children of this row. - gdc.writeStringToBuffer('\n '); + gdc.writeStringToBuffer("\n "); md.childLoop(tableRowElement); - gdc.writeStringToBuffer('\n '); + gdc.writeStringToBuffer("\n "); }; -html.handleTableCell = function(tableCellElement) { +html.handleTableCell = function (tableCellElement) { if (gdc.isSingleCellTable === true) { md.childLoop(tableCellElement); return; @@ -325,11 +350,13 @@ html.handleTableCell = function(tableCellElement) { gdc.startingTableCell = true; // Set attribute for colspan or rowspan, if necessary (>1). - var tdAttr = ''; - + var tdAttr = ""; + // Rowspan handling. var rowspan = tableCellElement.getRowSpan(); - if (rowspan === 0) { return; } + if (rowspan === 0) { + return; + } if (rowspan > 1) { tdAttr += ' rowspan="' + rowspan + '"'; @@ -338,66 +365,67 @@ html.handleTableCell = function(tableCellElement) { // Colspan handling. var colspan = tableCellElement.getColSpan(); - // Skip cells that have been merged over. - if (colspan === 0) { return; } - + // Skip cells that have been merged over. + if (colspan === 0) { + return; + } + if (colspan > 1) { tdAttr += ' colspan="' + colspan + '"'; } // End colspan code. - + // Add attribute only if non-empty. if (tdAttr) { - gdc.writeStringToBuffer('\n '); + gdc.writeStringToBuffer("\n "); } else { - gdc.writeStringToBuffer('\n '); + gdc.writeStringToBuffer("\n "); } - + // Go through children of this cell. md.childLoop(tableCellElement); md.maybeEndCodeBlock(); html.closeAllLists(); - gdc.writeStringToBuffer('\n '); + gdc.writeStringToBuffer("\n "); }; // Handle the heading type of the paragraph. // Need to close heading for HTML too, so need to save heading state. // Fall through for NORMAL. -html.handleHeading = function(heading, para) { - +html.handleHeading = function (heading, para) { // We're doing a little dance here for heading demotion. Also for closing tags. var htitle = 0; if (gdc.demoteHeadings) { htitle = 1; } switch (heading) { - case DocumentApp.ParagraphHeading.HEADING6: - // Warn about level 6 headings if demoting (and do not demote h6). - var warning = 'H6 not demoted to H7.'; + case DocumentApp.ParagraphHeading.HEADING6: + // Warn about level 6 headings if demoting (and do not demote h6). + var warning = "H6 not demoted to H7."; if (gdc.demoteHeadings) { if (!gdc.warnedAboutH7) { gdc.warn(warning + ' Look for "' + warning + '" inline.'); gdc.warnedAboutH7 = true; } - gdc.writeStringToBuffer('\n\n'); + gdc.writeStringToBuffer("\n\n"); } - gdc.writeStringToBuffer('\n'); + gdc.writeStringToBuffer(">"); }; // Close heading if necessary. (Blank line after to keep Markdown parser happy.) -html.closeHeading = function() { - +html.closeHeading = function () { // We're doing a little dance here for heading demotion. var htitle = 0; if (gdc.demoteHeadings) { htitle = 1; } - if (html.h1) { html.h1 = false; gdc.writeStringToBuffer('\n\n'); } - else if (html.h2) { html.h2 = false; gdc.writeStringToBuffer('\n\n'); } - else if (html.h3) { html.h3 = false; gdc.writeStringToBuffer('\n\n'); } - else if (html.h4) { html.h4 = false; gdc.writeStringToBuffer('\n\n'); } - else if (html.h5) { html.h5 = false; gdc.writeStringToBuffer('\n\n'); } - else if (html.h6) { html.h6 = false; gdc.writeStringToBuffer('\n\n'); } + if (html.h1) { + html.h1 = false; + gdc.writeStringToBuffer("\n\n"); + } else if (html.h2) { + html.h2 = false; + gdc.writeStringToBuffer("\n\n"); + } else if (html.h3) { + html.h3 = false; + gdc.writeStringToBuffer("\n\n"); + } else if (html.h4) { + html.h4 = false; + gdc.writeStringToBuffer("\n\n"); + } else if (html.h5) { + html.h5 = false; + gdc.writeStringToBuffer("\n\n"); + } else if (html.h6) { + html.h6 = false; + gdc.writeStringToBuffer("\n\n"); + } html.isHeading = false; }; // Formats footnotes for HTML. For HTML, we'll print out the actual // footnotes at the end. -html.handleFootnote = function(footnote) { - +html.handleFootnote = function (footnote) { gdc.hasFootnotes = true; - + gdc.footnoteNumber++; var fSection = footnote.getFootnoteContents(); if (!fSection) { @@ -471,26 +516,39 @@ html.handleFootnote = function(footnote) { var findex = gdc.footnoteNumber - 1; fSection = gdc.footnotes[findex].getFootnoteContents(); } - + // Write the footnote ref link in the text. - gdc.writeStringToBuffer('' + gdc.footnoteNumber + ''); + gdc.writeStringToBuffer( + '' + + gdc.footnoteNumber + + "" + ); // Now, write the footnotes themselves. gdc.isFootnote = true; // Open list for first footnote. if (gdc.footnoteNumber === 1) { - gdc.writeStringToBuffer('\n\n' - + '\n\n

Notes

' - + '\n
' - + '\n
' - + '\n
    '); + gdc.writeStringToBuffer( + "\n\n" + + "\n\n

    Notes

    " + + '\n
    ' + + "\n
    " + + "\n
      " + ); } // Each HTML footnote is a list item in an ordered list: gdc.writeStringToBuffer('
    1. '); md.childLoop(fSection); // Close footnote with a link back to the ref. - gdc.writeStringToBuffer(' '); + gdc.writeStringToBuffer( + ' ' + ); gdc.isFootnote = false; }; @@ -498,17 +556,16 @@ html.handleFootnote = function(footnote) { // For keeping track of nested HTML lists. html.listStack = []; -html.handleListItem = function(listItem) { - +html.handleListItem = function (listItem) { // Close definition list if we're in one now. if (gdc.inDlist) { gdc.closeDlist(); } - + var gt = listItem.getGlyphType(), - textElement = listItem.asText(), - attrix = textElement.getTextAttributeIndices(), - isList = true; + textElement = listItem.asText(), + attrix = textElement.getTextAttributeIndices(), + isList = true; html.nestingLevel = listItem.getNestingLevel(); @@ -517,43 +574,45 @@ html.handleListItem = function(listItem) { html.closeListItem(); } - gdc.listPrefix = ''; + gdc.listPrefix = ""; for (var i = 0; i < html.nestingLevel; i++) { - gdc.listPrefix += ' '; + gdc.listPrefix += " "; } - + // Determine what type of list before we open it (if necessary). - if (gt === DocumentApp.GlyphType.BULLET - || gt === DocumentApp.GlyphType.HOLLOW_BULLET - || gt === DocumentApp.GlyphType.SQUARE_BULLET) { + if ( + gt === DocumentApp.GlyphType.BULLET || + gt === DocumentApp.GlyphType.HOLLOW_BULLET || + gt === DocumentApp.GlyphType.SQUARE_BULLET + ) { gdc.listType = gdc.ul; html.maybeOpenList(listItem); } else { gdc.listType = gdc.ol; html.maybeOpenList(listItem); } - - gdc.writeStringToBuffer('\n'); + + gdc.writeStringToBuffer("\n"); // Note that ulItem, olItem are the same in HTML (
    2. ). gdc.writeStringToBuffer(gdc.listPrefix + gdc.htmlMarkup.ulItem); html.inListItem = true; md.childLoop(listItem); - + // Check to see if we should close this list. gdc.maybeCloseList(listItem); }; // Called to check if we're exiting a list as we enter a new element. // Maybe this should just be gdc.checklist(). -html.checkList = function() { +html.checkList = function () { if (gdc.isList && !gdc.indent) { html.closeAllLists(); gdc.isList = false; } }; // Closes list item. Not necessary for Markdown. -html.closeListItem = function() { - // Check if we're in a code block and end if so. Always close codeblocks before closing list items. +html.closeListItem = function () { + // Check if we're in a code block and end if so. Always close codeblocks before closing list items. if (gdc.inCodeBlock) { gdc.writeStringToBuffer(html.closeCodeBlock); gdc.inCodeBlock = false; @@ -571,28 +630,28 @@ html.maybeOpenList = function (listItem) { } // Open list if last sibling was not a list item. if (previousType !== DocumentApp.ElementType.LIST_ITEM) { - // We need to check if a list is already opened first. Is a global variable to track list level the best solution here? - // The previous sibling won't return the list level if it's a paragraph. + // We need to check if a list is already opened first. Is a global variable to track list level the best solution here? + // The previous sibling won't return the list level if it's a paragraph. // Could we also use: // if (html.nestingLevel == 0 && gdc.isList == false) { if (html.nestingLevel >= html.listNestingLevel) { html.openList(); - } + } } else if (previousType == DocumentApp.ElementType.LIST_ITEM) { // Open a new list if nesting level increases. - if (html.nestingLevel > previous.getNestingLevel()) { + if (html.nestingLevel > previous.getNestingLevel()) { html.openList(); - } + } } }; // Open list and save current list type to stack. -html.openList = function() { +html.openList = function () { gdc.isList = true; if (gdc.nestingLevel === 0) { - gdc.writeStringToBuffer('\n'); + gdc.writeStringToBuffer("\n"); } if (gdc.ul === gdc.listType) { @@ -608,7 +667,7 @@ html.openList = function() { }; // Close list and remove it's list type from the stack. -html.closeList = function() { +html.closeList = function () { // Close the last item of the list. This will always need to be called. html.closeListItem(); @@ -627,7 +686,7 @@ html.closeList = function() { }; // But what about a table that's in a list item? -html.closeAllLists = function() { +html.closeAllLists = function () { var list = html.listStack[0]; while (html.listStack.length > 0) { var list = html.listStack[0]; @@ -636,7 +695,7 @@ html.closeAllLists = function() { }; // Escape angle brackets so code blocks will display HTML tags. -html.escapeOpenTag = function(text) { - text = text.replace(/ { + if (comment.quotedText && output.includes(comment.quotedText)) { + output = output.replace( + comment.quotedText, + `${comment.quotedText}[${commentCounter}]` + ); + commentCounter++; + } + }); + + return output; +} + +// Create Markdown comment section +function createMarkdownCommentSection(comments) { + if (!comments || comments.length === 0) return ""; + + let output = "\n## Comments\n\n"; + let commentCounter = 1; + + comments.forEach((comment) => { + output += formatMarkdownComment(comment, commentCounter++); + output += "\n"; + }); + + return output; +} diff --git a/addon/sidebar.html b/addon/sidebar.html index e0d063f..4e8809a 100644 --- a/addon/sidebar.html +++ b/addon/sidebar.html @@ -127,6 +127,11 @@ Render HTML tags
      + +