Copying Google Doc Header Formatting with GAS - google-apps-script

I'm trying to copy a Header/Footer from the active Doc acting as a template to a newly created Doc. I'm able to get the text easy enough, but I'm not able to get the formatting, fonts or horizontal alignment.
My theory was that I could do something like
newDocHeader.setAttributes(activeDocHeader.getAttributes());
But, I still only see plain text that is left aligned. When inspecting the attributes object on the header i get the following:
({
FONT_SIZE:null,
ITALIC:null,
STRIKETHROUGH:null,
FOREGROUND_COLOR:null,
BOLD:null,
LINK_URL:null,
UNDERLINE:null,
FONT_FAMILY:null,
BACKGROUND_COLOR:null
})
I tried to loop through the Child objects of the Header and perform a similar setAttributes(getAttributes) on each child, but to no avail.
I also thought the copy() function on the Header/Footer object would be promising, but when I tried
newDocFooter = activeDocFooter.copy();
But, this produces a blank footer with no text or formatting.
Is there a good way to copy the formatting, font and horizontal alignment from one Header/Footer to another?

I'm entirely unfamiliar with DocumentApp, but this broadly worked for me:
/**
* Copies headers from one document to another.
* #param {string} source The source document URL.
* #param {string} target The target document URL.
*/
function copyHeader(source, target) {
// Used to map from child types to required "append<X>" method
var functionMap = {
PARAGRAPH: 'appendParagraph',
LIST_ITEM: 'appendListItem',
HORIZONTAL_RULE: 'appendHorizontalRule',
IMAGE: 'appendImage',
TABLE: 'appendTable'
};
var t = DocumentApp.openByUrl(target);
var s = DocumentApp.openByUrl(source);
var sourceHeader = s.getHeader();
var targetHeader = t.getHeader();
// If there is no header in the target doc, add one
if (!targetHeader) {
targetHeader = t.addHeader();
}
targetHeader.clear();
// Docs requires one child element, so one will remain even
// after clearing. Get a reference to it so it can be removed
var targetNumChild = targetHeader.getNumChildren();
if (targetNumChild === 1) {
var placeholderElement = targetHeader.getChild(0);
}
// Copy across each element to the target doc
var c = sourceHeader.getNumChildren();
for (var i = 0; i < c; i++) {
var element = sourceHeader.getChild(i).copy();
var method = functionMap[element.getType()];
targetHeader[method](element);
}
// Remove the saved element if required
if (targetHeader.getNumChildren() > 1 && placeholderElement) {
targetHeader.removeChild(placeholderElement);
}
}
I say broadly only because formatting such as bold, horizontal centering, horizontal rules etc all copied across fine, but bizarrely, lists seem to change from being numbered to being bulleted, so something was lost in translation.
It might need a little tweaking, and surely there is an easier way, but in the absence of anything else, this might be a start.
Source document:
Target document, note that the list type isn't quite right!:
Hope it helps.

Related

wrap google doc content with divs and turn into html file

I'm trying to turn google docs file into html file. I gave html tags to the content via google docs (h1, h2, p, etc) and then I downloaded it as HTML file and it works great.
I have one problem - I want to wrap specific contents in my google doc file with divs, a div for each chapter for example. Right now the file is just a list of html tags (h1, p, h2 etc) and I need it to be more correct hierarchically.
Is there a way to do that? I tried to use page break and other similar options but it just adds another element to the list and not wrapping a specific content in a div as I wish.
A javascript solution will be good too.
Would appreciate any help, Thanks!
Nir
You can try this approach on Google Apps Script. This is in no way the only solution. This is only a simple code you can try from many possible solutions available. Feel free to modify if needed depending on your use case.
Code:
function myFunction() {
var doc = DocumentApp.getActiveDocument();
var content = doc.getBody();
var numChildren = content.getNumChildren();
var output = [];
// Send email to this address
var sendTo = "test#gmail.com";
output.push("<html><body>");
for(var i=0; i < numChildren; i++){
var item = content.getChild(i).asText();
var text = item.getText();
// Assuming that a chapter always starts in bold headers, we start the div there
if(item.isBold()) {
// Add opening div tags on every start of header, see doc format below
output.push('<div>');
output.push('<h1>' + text + '</h1>');
}
// If not bold, then that element is assumed as the content of the chapter
else if (text){
output.push('<p>' + text + '</p>');
}
}
output.push("</body></html>");
// Get all indices where div is (except the first) and reverse
var indexes = getAllIndexes(output, "<div>");
// Per div found, insert closing div tag </div> before it
indexes.forEach(function(index){
output.splice(index, 0, "</div>");
});
// Join output array and treat it as html
var html = Utilities.newBlob(output.join(""), 'text/html', 'doc_to_html.html');
// Send to your email (modify email above if needed) the converted file with div tags
MailApp.sendEmail(sendTo, "Attached html-converted document", "file is attached below", {name: 'doc-to-html', attachments: [html], noReply: true});
}
function getAllIndexes(arr, val) {
var indexes = [], i = -1;
while ((i = arr.indexOf(val, i+1)) != -1){
indexes.push(i);
}
// Remove the first index (we don't need to add closing divs before the first div)
indexes.shift();
// Return reversed indices since we will add from the end since we are inserting closing div tags (</div>)
// Inserting from the start will change the indices of those succeeding opening div tags (<div>) we need to close
return indexes.reverse();
}
Email:
HTML Attachment:
Note:
This was assumed that per chapter, there is a single header at the start (we insert the <div> here), and paragraph/s below it. The closing div tags </divs> are inserted every before the next <div> tags found.

How do I get a Google Apps Script Document Outline from the API?

I am able to get the contents of a documents outline by searching all the paragraphs and seeing the heading each paragraph is using. This works but isn't ideal. If I manually remove an item from the outline, it doesn't change the heading of that item to normal. Therefore when I run my code again it'll detect the removed item as still being part of the outline since it's heading matches one used for the outline.
var searchHeading = DocumentApp.ParagraphHeading.HEADING4;
var paragraphs = getParagraphs();
return paragraphs.filter(function(paragraph) {
return (paragraph.header == searchHeading);
});
function getParagraphs() {
var paragraphs = DocumentApp.getActiveDocument().getBody().getParagraphs();
return paragraphs.map(function(p, index) {
return {
paragraph: p.getText(),
header: p.getHeading(),
id: index
};
});
}
Is there anyway to get the contents of the outline without parsing each paragraph and filtering those whose header matches a specific heading, ideally from an API call?
EDIT: Added getParagraphs()

Preserve highlight when copy from Ace Editor

I am using Ace Editor in my web app. Wonder if it's possible to copy the text inside Ace Editor to clipboard with highlight. With default configurations, if I copy the selected text in Ace Editor to clipboard, it seems that only text content is copied with no html styles.
Thanks a lot for your help!
Unfortunately there is no api for this. you'll need to modify https://github.com/ajaxorg/ace/blob/v1.2.5/lib/ace/keyboard/textinput.js#L290 to also set text/html mime type to some html, rendered similar to https://github.com/ajaxorg/ace/blob/v1.2.5/lib/ace/layer/text.js#L442.
Also you'll need to include the css for the theme in the copied html
I know this is late, but this might be helpful for someone like me who stumbled upon this problem this late.
The basic idea is to get the text that is being copied and use Ace's tokenizer to generate HTML from it:
Add an event listener on the copy/cut event on the editor's container.
You can use clipboard object in event to get the data currently being copied: event.clipboardData?.getData('text/plain')
Rest steps are in code below
// get current tokenizer
const tokenizer = aceSession.getMode().getTokenizer();
// get `Text` object from ace , this will help in generating HTML
const Text = ace.require('ace/layer/text').Text;
// create a wrapper div, all your resultant HTML will come inside this
// also this will contain the basic HTML required to initialize the editor
const root = document.createElement('div');
// this is the main magic object
const rootText = new Text(root);
lines.forEach(line => {
// this converts your text to tokens
const tokens = tokenizer.getLineTokens(line, 'start') as any;
const leadingSpacesCount = (line.match(/^\s*/) || [])[0].length;
const lineGroupEl = document.createElement('div');
lineGroupEl.className = 'ace_line_group';
const lineEl = document.createElement('div');
lineEl.className = 'ace_line';
const spaceSpan = document.createElement('span');
if (tokens && tokens.tokens.length) {
//////////////////////////////////////////////////////
// Magic Happens here, this line is responsible for converting our tokens to HTML elements
rootText.$renderSimpleLine(lineEl, tokens.tokens);
//////////////////////////////////////////////////////
// Leading spaces do not get translated to HTML, add them separately
spaceSpan.innerHTML = ' '.repeat(leadingSpacesCount);
lineEl.insertBefore(spaceSpan, lineEl.children[0]);
} else {
spaceSpan.innerHTML = ' ';
lineEl.appendChild(spaceSpan);
}
lineGroupEl.appendChild(lineEl);
// `root` has a wrapper div, inside which our main div (with class "ace_layer ace_text-layer") lies
root.children[0].appendChild(lineGroupEl);
});
return root.innerHTML;
Now finally, in your eventlistener you can wrap this with any div to give your own custom color to it, and put it to clipboardData with text\html mime type:
event.clipboardData?.setData('text/html', htmlContent);

html5 template tag: fallback for content access

I am using a template tag in a webkit browser (JavaFX WebView 2.2) to store elements that I may clone and append on the main part of the document.
However, I can't access its content using templateElement.content (the HTML5 standard). Instead, I use jQuery to get the elements inside the template TAG with the selector "#templateElement div".
Seems the template tag is not yet fully supported (inner scripts also run), although its contents are not rendered.
My fear is that, when the template tag becomes supported, the way to get its contents will break and my page will stop working.
What is the recommended way of getting template contents regardless future implementation changes?
HTML:
<template id="templateElement">
<div>Clone Me!</div>
</template>
JavaScript:
function getContentsCurrent() {
var toBeCloned = $("#templateElement div")[0];
//append where needed...
}
function getContentsFuture() {
var toBeCloned = templateElement.content.getElementsByTagName("div")[0];
//append where needed...
}
EDIT
I think jQuery won't be able to handle this automatically, even in the future, because the template "innerHTML" is purposely routed to content so that it becomes inaccessible to the DOM (so no selector touches it accidentally).
You could test if the content feature exists before:
function getContents() {
var toBeCloned;
if ( templateElement.content )
toBeCloned = templateElement.content.getElementsByTagName("div")[0];
else
toBeCloned = templateElement.querySelector("div");
//append where needed...
}
Another way:
var content = templateElement.content || templateElement
var toBeCloned = content.querySelector( "div" )
//...

Is there a way to select an HTML node and copy only the CSS for that node? [duplicate]

I often find nice stylings on the web. To copy the CSS of a DOM element, I inspect that element with Google Chrome Developer Tools, look at the various CSS properties, and copy those manually to my own stylesheets.
Is it possible to easily export all CSS properties of a given DOM element?
Here is the code for an exportStyles() method that should return a CSS string including all inline and external styles for a given element, except default values (which was the main difficulty).
For example: console.log(someElement.exportStyles());
Since you are using Chrome, I did not bother making it compatible with IE.
Actually it just needs that the browsers supports the getComputedStyle(element) method.
Element.prototype.exportStyles = (function () {
// Mapping between tag names and css default values lookup tables. This allows to exclude default values in the result.
var defaultStylesByTagName = {};
// Styles inherited from style sheets will not be rendered for elements with these tag names
var noStyleTags = {"BASE":true,"HEAD":true,"HTML":true,"META":true,"NOFRAME":true,"NOSCRIPT":true,"PARAM":true,"SCRIPT":true,"STYLE":true,"TITLE":true};
// This list determines which css default values lookup tables are precomputed at load time
// Lookup tables for other tag names will be automatically built at runtime if needed
var tagNames = ["A","ABBR","ADDRESS","AREA","ARTICLE","ASIDE","AUDIO","B","BASE","BDI","BDO","BLOCKQUOTE","BODY","BR","BUTTON","CANVAS","CAPTION","CENTER","CITE","CODE","COL","COLGROUP","COMMAND","DATALIST","DD","DEL","DETAILS","DFN","DIV","DL","DT","EM","EMBED","FIELDSET","FIGCAPTION","FIGURE","FONT","FOOTER","FORM","H1","H2","H3","H4","H5","H6","HEAD","HEADER","HGROUP","HR","HTML","I","IFRAME","IMG","INPUT","INS","KBD","KEYGEN","LABEL","LEGEND","LI","LINK","MAP","MARK","MATH","MENU","META","METER","NAV","NOBR","NOSCRIPT","OBJECT","OL","OPTION","OPTGROUP","OUTPUT","P","PARAM","PRE","PROGRESS","Q","RP","RT","RUBY","S","SAMP","SCRIPT","SECTION","SELECT","SMALL","SOURCE","SPAN","STRONG","STYLE","SUB","SUMMARY","SUP","SVG","TABLE","TBODY","TD","TEXTAREA","TFOOT","TH","THEAD","TIME","TITLE","TR","TRACK","U","UL","VAR","VIDEO","WBR"];
// Precompute the lookup tables.
for (var i = 0; i < tagNames.length; i++) {
if(!noStyleTags[tagNames[i]]) {
defaultStylesByTagName[tagNames[i]] = computeDefaultStyleByTagName(tagNames[i]);
}
}
function computeDefaultStyleByTagName(tagName) {
var defaultStyle = {};
var element = document.body.appendChild(document.createElement(tagName));
var computedStyle = getComputedStyle(element);
for (var i = 0; i < computedStyle.length; i++) {
defaultStyle[computedStyle[i]] = computedStyle[computedStyle[i]];
}
document.body.removeChild(element);
return defaultStyle;
}
function getDefaultStyleByTagName(tagName) {
tagName = tagName.toUpperCase();
if (!defaultStylesByTagName[tagName]) {
defaultStylesByTagName[tagName] = computeDefaultStyleByTagName(tagName);
}
return defaultStylesByTagName[tagName];
}
return function exportStyles() {
if (this.nodeType !== Node.ELEMENT_NODE) {
throw new TypeError("The exportStyles method only works on elements, not on " + this.nodeType + " nodes.");
}
if (noStyleTags[this.tagName]) {
throw new TypeError("The exportStyles method does not work on " + this.tagName + " elements.");
}
var styles = {};
var computedStyle = getComputedStyle(this);
var defaultStyle = getDefaultStyleByTagName(this.tagName);
for (var i = 0; i < computedStyle.length; i++) {
var cssPropName = computedStyle[i];
if (computedStyle[cssPropName] !== defaultStyle[cssPropName]) {
styles[cssPropName] = computedStyle[cssPropName];
}
}
var a = ["{"];
for(var i in styles) {
a[a.length] = i + ": " + styles[i] + ";";
}
a[a.length] = "}"
return a.join("\r\n");
}
})();
This code is base on my answer for a slightly related question: Extract the current DOM and print it as a string, with styles intact
I'm quoting Doozer Blake's excellent answer, provided above as a comment. If you like this answer, please upvote his original comment above:
Not a direct answer, but with Chrome Developer Tools, you can click inside Styles or Computed Styles, hit Ctrl+A and then Ctrl+C to copy all the styles in those given areas. It's not perfect in the Style tab because it picks up some extra stuff. Better than selecting them one by one I guess. – Doozer Blake 3 hours ago
You can do the same using Firebug for Firefox, by using Firebug's "Computed" side panel.
There are a few ways to almost do this.
Have a look at FireDiff
Also have a look at cssUpdater This is for local CSS only]
And see this Q for more similar tools: Why can't I save CSS changes in Firebug?
Also this paid product claims to be able to do this: http://www.skybound.ca/