| /** | | | * Format every table in the active Google Doc. | | | * | | | * - Body text: Arial 8 | | | * - Header row: #0b5394 fill, white bold Arial 8 | | | * - Cell padding: 0 on all sides | | | * - Text edges: 0.02" inset on left & right via paragraph indents | | | * (the blue ruler controls), not via cell padding. | | | * | | | * Run formatAllTables(). First run will prompt for authorization. | | | */ | |
| // 0.02 inch, expressed in points (1 in = 72 pt). | |
| var TEXT_INSET_PT = 0.02 * 72; // = 1.44 pt | |
| var FONT_FAMILY = 'Arial'; | |
| var FONT_SIZE = 8; | |
| var HEADER_FILL = '#0b5394'; | |
| var HEADER_TEXT = '#ffffff'; | |
| // Body rows alternate between these two fills (first body row gets [0]). | |
| var ROW_FILLS = ['#f9f9f9', '#efefef']; | |
| function formatAllTables() { | |
| var body = DocumentApp.getActiveDocument().getBody(); | |
| var tables = body.getTables(); | |
| for (var t = 0; t < tables.length; t++) { | |
| formatTable(tables[t], t + 1); | |
| } | | | // Borders are set via the advanced Docs API (DocumentApp can't override | | | // per-cell borders). Requires the "Docs API" advanced service enabled. | | | setWhiteBorders(); | | | // Turn off space-hungry pagination controls across the whole doc. | | | setCompactPagination(); | | | // Body paragraphs (outside tables): 0 before / 4 after. | | | setBodyParagraphSpacing(); | | | } | | | /** | | | * Set spacing to 0pt before / 4pt after on every paragraph OUTSIDE tables. | | | * Top-level paragraph elements are, by definition, not inside table cells, | | | * so iterating them leaves the table spacing rules untouched. | | | * | | | * REQUIRES the "Google Docs API" advanced service. | | | */ | |
| function setBodyParagraphSpacing() { | |
| var docId = DocumentApp.getActiveDocument().getId(); | |
| var doc = Docs.Documents.get(docId); | |
| var content = doc.body.content || []; | |
| var requests = []; | |
| for (var i = 0; i < content.length; i++) { | |
| var el = content[i]; | |
| if (!el.paragraph) continue; // skip tables, section breaks, etc. | |
| requests.push({ | |
| updateParagraphStyle: { | |
| paragraphStyle: { | |
| spaceAbove: { magnitude: 0, unit: 'PT' }, | |
| spaceBelow: { magnitude: 4, unit: 'PT' } | |
| }, | | | fields: 'spaceAbove,spaceBelow', | | | range: { startIndex: el.startIndex, endIndex: el.endIndex } | | | } | | | }); | | | } | |
| if (requests.length > 0) { | |
| Docs.Documents.batchUpdate({ requests: requests }, docId); | |
| Logger.log('Set 0/4 spacing on %s body paragraph(s).', requests.length); | |
| } | | | } | | | /** | | | * Disable the four "space-hungry" pagination settings on EVERY paragraph | | | * in the document (body + tables): Keep with next, Keep lines together, | | | * Prevent single lines (widow/orphan), and Page break before. | | | * | | | * REQUIRES the "Google Docs API" advanced service. | | | */ | |
| function setCompactPagination() { | |
| var docId = DocumentApp.getActiveDocument().getId(); | |
| var doc = Docs.Documents.get(docId); | |
| var content = doc.body.content; | |
| if (!content || content.length === 0) return; | |
| var endIndex = content[content.length - 1].endIndex; | |
| // These three are allowed on paragraphs inside tables, so apply doc-wide. | |
| var requests = [{ | |
| updateParagraphStyle: { | |
| paragraphStyle: { | |
| keepWithNext: false, | | | keepLinesTogether: false, | | | avoidWidowAndOrphan: false | | | }, | | | fields: 'keepWithNext,keepLinesTogether,avoidWidowAndOrphan', | | | range: { startIndex: 1, endIndex: endIndex - 1 } | | | } | | | }]; | | | // pageBreakBefore is rejected on in-table paragraphs, so only target | | | // top-level paragraphs (which are never inside a table). | |
| for (var i = 0; i < content.length; i++) { | |
| var el = content[i]; | |
| if (!el.paragraph) continue; | |
| requests.push({ | |
| updateParagraphStyle: { | |
| paragraphStyle: { pageBreakBefore: false }, | |
| fields: 'pageBreakBefore', | | | range: { startIndex: el.startIndex, endIndex: el.endIndex } | | | } | | | }); | | | } | |
| Docs.Documents.batchUpdate({ requests: requests }, docId); | |
| Logger.log('Disabled keep-with-next / keep-together / widow-orphan doc-wide; page-break-before on body paragraphs.'); | |
| } | |
| function formatTable(table, tableNum) { | |
| var numRows = table.getNumRows(); | |
| // Top-left cell text (row 1, cell 1), for identifying tables in the log. | |
| var firstCellText = '(empty)'; | |
| if (numRows > 0 && table.getRow(0).getNumCells() > 0) { | |
| firstCellText = table.getRow(0).getCell(0).getText(); | |
| } | | | // Figure tables hold images in 1 or 2 cells β leave them untouched. | |
| var totalCells = 0; | |
| for (var i = 0; i < numRows; i++) { | |
| totalCells += table.getRow(i).getNumCells(); | |
| } | |
| if (totalCells <= 2) { | |
| Logger.log('Table %s: SKIPPED (%s cells) β cell(1,1): "%s"', tableNum, totalCells, firstCellText); | |
| return; | | | } | |
| Logger.log('Table %s: formatting (%s cells) β cell(1,1): "%s"', tableNum, totalCells, firstCellText); | |
| for (var r = 0; r < numRows; r++) { | |
| var row = table.getRow(r); | |
| var numCells = row.getNumCells(); | |
| var isHeader = (r === 0); | |
| // Body row index is r-1 so the first body row gets ROW_FILLS[0]. | |
| var bodyFill = isHeader ? null : ROW_FILLS[(r - 1) % ROW_FILLS.length]; | |
| for (var c = 0; c < numCells; c++) { | |
| var cell = row.getCell(c); | |
| formatCell(cell, isHeader, bodyFill); | |
| } | | | } | | | } | | | function formatCell(cell, isHeader, bodyFill) { | | | // Zero cell padding on all sides. | |
| cell.setPaddingTop(0); | |
| cell.setPaddingBottom(0); | |
| cell.setPaddingLeft(0); | |
| cell.setPaddingRight(0); | |
| // (Vertical centering is done via the Docs API in setWhiteBorders β | | | // DocumentApp's setVerticalAlignment silently no-ops here.) | | | // Fill: header color, otherwise the alternating body color. | |
| if (isHeader) { | |
| cell.setBackgroundColor(HEADER_FILL); | |
| } else { | |
| cell.setBackgroundColor(bodyFill); | |
| } | | | // Text attributes for everything inside the cell. | |
| var textAttrs = {}; | |
| textAttrs[DocumentApp.Attribute.FONT_FAMILY] = FONT_FAMILY; | |
| textAttrs[DocumentApp.Attribute.FONT_SIZE] = FONT_SIZE; | |
| textAttrs[DocumentApp.Attribute.BOLD] = isHeader; | |
| if (isHeader) { | |
| textAttrs[DocumentApp.Attribute.FOREGROUND_COLOR] = HEADER_TEXT; | |
| } else { | |
| textAttrs[DocumentApp.Attribute.FOREGROUND_COLOR] = null; // leave default | | | } | | | // Paragraph attributes: inset the text edges 0.02" on both sides | | | // using indents (the blue ruler markers), since padding is 0. | |
| var paraAttrs = {}; | |
| paraAttrs[DocumentApp.Attribute.INDENT_START] = TEXT_INSET_PT; | |
| paraAttrs[DocumentApp.Attribute.INDENT_END] = TEXT_INSET_PT; | |
| paraAttrs[DocumentApp.Attribute.INDENT_FIRST_LINE] = TEXT_INSET_PT; | |
| // Headers centered, body left-justified. | | | paraAttrs[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = isHeader | | | ? DocumentApp.HorizontalAlignment.CENTER | | | : DocumentApp.HorizontalAlignment.LEFT; | | | // Single line spacing inside tables. | |
| paraAttrs[DocumentApp.Attribute.LINE_SPACING] = 1; | |
| var numChildren = cell.getNumChildren(); | |
| for (var i = 0; i < numChildren; i++) { | |
| var child = cell.getChild(i); | |
| var type = child.getType(); | |
| if (type === DocumentApp.ElementType.PARAGRAPH) { | |
| var para = child.asParagraph(); | |
| para.setAttributes(paraAttrs); | |
| para.setSpacingAfter(0); // no space after plain paragraphs/line breaks | |
| para.editAsText().setAttributes(textAttrs); | |
| } else if (type === DocumentApp.ElementType.LIST_ITEM) { | |
| var li = child.asListItem(); | |
| li.setAttributes(paraAttrs); | |
| li.setSpacingAfter(2); // 2pt after bullets | |
| li.editAsText().setAttributes(textAttrs); | |
| } else if (type === DocumentApp.ElementType.TABLE) { | |
| // Nested table β getTables() doesn't return these, so recurse. | | | formatTable(child.asTable(), 'nested'); | | | } | | | } | | | } | | | /** | | | * Set every cell border to white 0.5pt via the advanced Docs API. | | | * DocumentApp can't reliably override per-cell borders, so we use | | | * updateTableCellStyle, which does. | | | * | | | * REQUIRES: Apps Script editor β Services (+) β add "Google Docs API". | | | */ | |
| function setWhiteBorders() { | |
| var docId = DocumentApp.getActiveDocument().getId(); | |
| var doc = Docs.Documents.get(docId); | |
| var requests = []; | |
| collectBorderRequests(doc.body.content, requests); | |
| if (requests.length > 0) { | |
| Docs.Documents.batchUpdate({ requests: requests }, docId); | |
| Logger.log('Applied white borders to %s table(s).', requests.length); | |
| } else { | |
| Logger.log('No eligible tables found for borders.'); | |
| } | | | } | | | // Walk structural elements, emit one border request per eligible table | |
| // (and recurse into nested tables). Mirrors the <=2-cell skip rule. | |
| function collectBorderRequests(content, requests) { | |
| if (!content) return; | |
| for (var i = 0; i < content.length; i++) { | |
| var el = content[i]; | |
| if (!el.table) continue; | |
| var table = el.table; | |
| var tableRows = table.tableRows || []; | |
| // Count cells; skip figure tables (1 or 2 cells total). | |
| var totalCells = 0; | |
| for (var r = 0; r < tableRows.length; r++) { | |
| totalCells += (tableRows[r].tableCells || []).length; | |
| } | |
| if (totalCells <= 2) continue; | |
| var whiteBorder = { | |
| color: { color: { rgbColor: { red: 1, green: 1, blue: 1 } } }, | |
| width: { magnitude: 0.5, unit: 'PT' }, | |
| dashStyle: 'SOLID' | |
| }; | |
| requests.push({ | |
| updateTableCellStyle: { | |
| tableCellStyle: { | |
| borderTop: whiteBorder, | | | borderBottom: whiteBorder, | | | borderLeft: whiteBorder, | | | borderRight: whiteBorder, | | | contentAlignment: 'MIDDLE' | | | }, | | | fields: 'borderTop,borderBottom,borderLeft,borderRight,contentAlignment', | | | tableStartLocation: { index: el.startIndex } | | | } | | | }); | | | // Recurse into nested tables inside each cell. | |
| for (var rr = 0; rr < tableRows.length; rr++) { | |
| var cells = tableRows[rr].tableCells || []; | |
| for (var cc = 0; cc < cells.length; cc++) { | |
| collectBorderRequests(cells[cc].content, requests); | |
| } | | | } | | | } | | | } |