/**
* Runs automatically when the document is opened.
*/
function onOpen() {
DocumentApp.getUi()
.createMenu('TOC Tools')
.addItem('Update Table of Contents Now', 'updateTOC')
.addToUi();
updateTOC(); // Auto-update on open
}
/**
/ ======================================================================================
/ ======================================================================================
/ AK TABLE OF CONTENTS TOOLS!!!!!! CAN RUN ON OPN
/ AK TABLE OF CONTENTS TOOLS!!!!!! CAN RUN ON OPN
function onOpen() {
DocumentApp.getUi()
.createMenu('TOC Tools')
.addItem('Update Table of Contents Now', 'updateTOC')
.addToUi();
updateTOC(); // Auto-update on open
}
*/
/**
* Builds or updates the Table of Contents at the top.
*/
function updateTOC() {
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
removeExistingTOC(body);
const headings = collectHeadings(body);
// Feedback (assuming you have a showToast function defined)
if (headings.length === 0) {
showToast("No headings (Heading 1–3) found. Check paragraph styles.", "TOC Update", 10);
} else {
showToast(`Found ${headings.length} headings. TOC updated.`, "TOC Update", 5);
}
if (headings.length === 0) return;
// Insert title (already black and centered)
// Insert title
const tocTitle = body.insertParagraph(0, 'Table of Contents');
tocTitle.setHeading(DocumentApp.ParagraphHeading.HEADING1)
.setAlignment(DocumentApp.HorizontalAlignment.CENTER)
.setAttributes({
BOLD: true,
FONT_SIZE: 18,
FOREGROUND_COLOR: '#000000'
});
// Set Calibri for the title too
const titleText = tocTitle.editAsText();
titleText.setFontFamily(0, titleText.getText().length - 1, 'Calibri');
body.insertParagraph(1, ''); // blank line
let insertPos = 2;
headings.forEach(heading => {
const p = body.insertParagraph(insertPos++, heading.text.trim());
const indent = (heading.level - 1) * 36;
p.setIndentStart(indent)
.setIndentEnd(0)
.setIndentFirstLine(0);
// Get the text object for fine-grained control
const textObj = p.editAsText(); // This gives access to the full text range
const fullLength = textObj.getText().length;
// Set font family to Calibri for the entire entry
textObj.setFontFamily(0, fullLength - 1, 'Calibri');
// Apply black color and NO underline to the entire text
textObj.setForegroundColor(0, textObj.getText().length - 1, '#000000');
textObj.setUnderline(0, textObj.getText().length - 1, false);
// NOW set the link — Docs will try to force blue/underline, but we override after
textObj.setLinkUrl(0, textObj.getText().length - 1, '#bookmark=' + heading.bookmarkId);
// Critical override: re-apply styles AFTER setting the link
textObj.setForegroundColor(0, textObj.getText().length - 1, '#000000');
textObj.setUnderline(0, textObj.getText().length - 1, false);
});
body.insertHorizontalRule(insertPos);
}
function showToast(message, title = "Info", timeoutSeconds = 4) {
const html = HtmlService.createHtmlOutput(
`<div style="padding: 12px; font-family: Arial; font-size: 14px;">
<strong>${title}</strong><br>
${message}
</div>`
)
.setWidth(280)
.setHeight(80);
const ui = DocumentApp.getUi();
ui.showModelessDialog(html, " "); // empty title bar looks cleaner
// Auto-close after timeout (client-side JS)
if (timeoutSeconds > 0) {
html.append(`<script>
setTimeout(() => google.script.host.close(), ${timeoutSeconds * 1000});
</script>`);
}
}
function removeExistingTOC(body) {
const toRemove = [];
let foundTitle = false;
showToast("Starting TOC cleanup scan...", "Debug", 3);
for (let i = 0; i < body.getNumChildren(); i++) {
const child = body.getChild(i);
if (child.getType() !== DocumentApp.ElementType.PARAGRAPH) {
// Remove HR or other non-paragraph if after title
if (foundTitle) {
toRemove.push(i);
}
continue;
}
const p = child.asParagraph();
const textTrim = p.getText().trim();
const textLower = textTrim.toLowerCase();
const isHeading = p.getHeading() !== DocumentApp.ParagraphHeading.NORMAL_TEXT;
// Detect title (very forgiving)
if (!foundTitle && textLower.includes('table') && textLower.includes('contents')) {
foundTitle = true;
toRemove.push(i);
showToast("TOC title found at position " + i, "Debug", 4);
continue;
}
if (foundTitle) {
// Keep removing while:
// - blank
// - OR has link (bookmark link)
// - OR indented
// - OR NOT a heading
const textObj = p.editAsText();
const hasLink = textObj.getText().length > 0 && textObj.getLinkUrl() !== null;
const isIndented = p.getIndentStart() > 0;
const isBlank = textTrim === '';
if (isBlank || hasLink || isIndented || !isHeading) {
toRemove.push(i);
showToast("Removing TOC-like item at " + i + " (link=" + hasLink + ", indent=" + isIndented + ")", "Debug", 2);
} else {
// This looks like your first real section heading → STOP and DO NOT remove it
showToast("Stop at position " + i + " — real heading detected", "Debug", 5);
break;
}
if (toRemove.length > 60) break; // safety
}
}
if (toRemove.length > 0) {
showToast("Preparing to remove " + toRemove.length + " items", "Info", 4);
toRemove.sort((a, b) => b - a);
toRemove.forEach(idx => {
try {
body.removeChild(body.getChild(idx));
} catch (e) {
showToast("Remove failed at " + idx + ": " + e.message, "Error", 6);
}
});
showToast("Cleanup finished — removed " + toRemove.length + " items", "Success", 5);
} else {
showToast("No TOC detected at top — nothing removed", "Warning", 6);
}
}
/**
* Collect headings with reliable bookmark handling
*/
/**
* Collect headings with reliable bookmark handling (no findElement on Body)
*/
function collectHeadings(body) {
const doc = DocumentApp.getActiveDocument();
const headings = [];
for (let i = 0; i < body.getNumChildren(); i++) {
const child = body.getChild(i);
if (child.getType() !== DocumentApp.ElementType.PARAGRAPH) continue;
const p = child.asParagraph();
const headingType = p.getHeading();
if (![DocumentApp.ParagraphHeading.HEADING1,
DocumentApp.ParagraphHeading.HEADING2,
DocumentApp.ParagraphHeading.HEADING3].includes(headingType)) continue;
const text = p.getText().trim();
if (text === '') continue;
// Get the Text element reliably (most paragraphs have exactly one Text child)
let textElement = null;
if (p.getNumChildren() > 0) {
const potentialText = p.getChild(0);
if (potentialText.getType() === DocumentApp.ElementType.TEXT) {
textElement = potentialText.asText();
}
}
// Fallback: If no Text child (very rare for headings), use editAsText() which gives a Text element
if (!textElement) {
textElement = p.editAsText();
}
// Now create position at offset 0 (start of the text)
const position = doc.newPosition(textElement, 0);
const bookmark = doc.addBookmark(position);
headings.push({
level: getHeadingLevel(headingType),
text: text,
bookmarkId: bookmark.getId()
});
}
return headings;
}
function getHeadingLevel(heading) {
switch (heading) {
case DocumentApp.ParagraphHeading.HEADING1: return 1;
case DocumentApp.ParagraphHeading.HEADING2: return 2;
case DocumentApp.ParagraphHeading.HEADING3: return 3;
default: return 0;
}
}
/**
/ ======================================================================================
/ ======================================================================================
/ END AK TABLE OF CONTENTS TOOLS!!!!!! CAN RUN ON OPN
/ END AK TABLE OF CONTENTS TOOLS!!!!!! CAN RUN ON OPN */
No comments:
Post a Comment