Chapter 4 : Code Snippets for Apps Script integration with Google Docs (Bounded script)
1/**
2 * Handles GET requests to the web app.
3 * Expects a 'docId' parameter and returns the document's content as Markdown.
4 */
5function doGet(e) {
6 try {
7 // Validate the presence of 'docId' parameter
8 if (!e.parameter.docId) {
9 return ContentService.createTextOutput(JSON.stringify({
10 success: false,
11 error: "Missing 'docId' parameter."
12 })).setMimeType(ContentService.MimeType.JSON);
13 }
14
15 var docId = e.parameter.docId;
16 var doc;
17 try {
18 doc = DocumentApp.openById(docId);
19 } catch (err) {
20 return ContentService.createTextOutput(JSON.stringify({
21 success: false,
22 error: "Invalid 'docId' or insufficient permissions."
23 })).setMimeType(ContentService.MimeType.JSON);
24 }
25
26 // Document title
27 var docTitle = doc.getName();
28
29 // Get all top-level tabs
30 var topLevelTabs = doc.getTabs();
31 var tabsData = topLevelTabs.map(function(tab) {
32 return traverseTabAsMarkdown(tab);
33 });
34
35 // Structure the response
36 var response = {
37 success: true,
38 document: {
39 id: docId,
40 title: docTitle,
41 tabs: tabsData
42 }
43 };
44
45 return ContentService
46 .createTextOutput(JSON.stringify(response))
47 .setMimeType(ContentService.MimeType.JSON);
48
49 } catch (error) {
50 return ContentService
51 .createTextOutput(JSON.stringify({
52 success: false,
53 error: "An unexpected error occurred.",
54 details: error.toString()
55 }))
56 .setMimeType(ContentService.MimeType.JSON);
57 }
58}
59
60/**
61 * Recursively traverse a tab and build a Markdown string from its content.
62 * Also handle any child tabs.
63 *
64 * @param {Tab} tab - The Tab object to traverse.
65 * @return {Object} - { id, title, content } with markdown content
66 */
67function traverseTabAsMarkdown(tab) {
68 var tabId = tab.getId();
69 var tabTitle = tab.getTitle();
70
71 var documentTab = tab.asDocumentTab();
72 var body = documentTab.getBody();
73
74 // Convert the body to a Markdown string
75 var markdownContent = bodyToMarkdown(body);
76
77 // Recursively process child tabs
78 var childTabs = tab.getChildTabs();
79 var subTabs = childTabs.map(function(childTab) {
80 return traverseTabAsMarkdown(childTab);
81 });
82
83 return {
84 id: tabId,
85 title: tabTitle,
86 content: markdownContent,
87 subTabs: subTabs
88 };
89}
90
91/**
92 * Converts the entire Body of a DocumentTab into a single Markdown string,
93 * ignoring headings beyond HEADING2 and converting bold text to **bold**.
94 *
95 * @param {Body} body - The DocumentTab's body.
96 * @return {string} - Markdown representation of the content.
97 */
98function bodyToMarkdown(body) {
99 var markdown = [];
100 var numChildren = body.getNumChildren();
101
102 for (var i = 0; i < numChildren; i++) {
103 var element = body.getChild(i);
104 if (element.getType() === DocumentApp.ElementType.PARAGRAPH) {
105 var paragraph = element.asParagraph();
106 var heading = paragraph.getHeading();
107
108 // Convert the paragraph to a Markdown line (with possible bold text)
109 var mdLine = paragraphToMarkdown(paragraph);
110
111 // Decide how to label the line based on heading level
112 if (heading === DocumentApp.ParagraphHeading.HEADING1) {
113 // # Heading
114 mdLine = "# " + mdLine;
115 } else if (heading === DocumentApp.ParagraphHeading.HEADING2) {
116 // ## Heading
117 mdLine = "## " + mdLine;
118 } else if (heading === DocumentApp.ParagraphHeading.HEADING3 ) {
119 mdLine = '###' + mdLine
120 } else if
121 (
122 heading === DocumentApp.ParagraphHeading.HEADING4 ||
123 heading === DocumentApp.ParagraphHeading.HEADING5 ||
124 heading === DocumentApp.ParagraphHeading.HEADING6
125 ) {
126
127 continue;
128 }
129
130 // Append to our final Markdown
131 // (skip entirely if blank—often empty paragraphs occur)
132 if (mdLine.trim() !== "") {
133 markdown.push(mdLine);
134 }
135 }
136 // Note: ignoring tables, images, etc. as requested
137 }
138
139 // Combine into a single string with newlines
140 return markdown.join("\n\n");
141}
142
143/**
144 * Converts a single paragraph into a Markdown string, detecting bold text.
145 * Everything else (italics, underline, etc.) is left plain by default.
146 *
147 * @param {Paragraph} paragraph - The paragraph to process.
148 * @return {string} - The paragraph text with **bold** segments in Markdown.
149 */
150function paragraphToMarkdown(paragraph) {
151 var mdParts = [];
152 var numChild = paragraph.getNumChildren();
153
154 for (var i = 0; i < numChild; i++) {
155 var child = paragraph.getChild(i);
156 if (child.getType() === DocumentApp.ElementType.TEXT) {
157 var textElement = child.asText();
158 var textStyle = textElement.getTextAttributeIndices();
159 // We'll iterate by style runs
160 for (var s = 0; s < textStyle.length; s++) {
161 var startOffset = textStyle[s];
162 var endOffset = (s === textStyle.length - 1)
163 ? textElement.getText().length - 1
164 : textStyle[s + 1] - 1;
165
166 var segment = textElement.getText().substring(startOffset, endOffset + 1);
167
168 // Check if this range is bold
169 var isBold = textElement.isBold(startOffset);
170
171 if (isBold) {
172 mdParts.push("**" + segment + "**");
173 } else {
174 mdParts.push(segment);
175 }
176 }
177 }
178 }
179
180 // Join all text/bold runs into one line
181 return mdParts.join("");
182}
183