I'm trying to execute a mailmerge script that reads fields from a spreadsheet and the template from a google doc. The script runs on the google doc.
The script works fine except it doesn't copy the formatting or table that is in the document. This is crucial.
Below is the code I am using.
function mmerge() {
var mdoc=DocumentApp.getActiveDocument();
var mdat=SpreadsheetApp.openById("1Nc2xu--").getSheetByName("MailMerge List);
var numcols=mdat.getDataRange().getNumColumns();
var numrows=mdat.getDataRange().getNumRows();
var mtxt=mdoc.getBody().getText();
var flds=[];
for (var j=0;j<numcols;j++) {flds.push(mdat.getRange(1, j+1).getValue());}
var rtxt;
for (var i=2; i<=numrows; i++) {
rtxt=mtxt
for (var j=1; j<=numcols; j++) {
rtxt=rtxt.replace("{"+flds[j-1]+"}",mdat.getRange(i, j).getValue());
}
mdoc.getBody().appendParagraph(rtxt);
}
}
I suspect that it has to do with the method used here: mtxt=mdoc.getBody().getText() but I don't know how to change it. My attempts to change it to copy() results in an error.
Does anyone know what I am doing wrong? Any help would be greatly appreciated.
Update: This is not a straight forward copy problem: the process involves having to execute the mail merge and then apply the formatting to the mailmerged doc.
Thanks
getText() returns a String, which has no character formatting.
The greater issue is that G Suite Services in Google Apps Script does not provide the document as HTML. (The getAs() method on the Document object only allows you to retrieve it as a PDF.)
You can adapt the "Google Doc to clean HTML converter" script, although it won't be trivial. It currently does not convert table content into HTML tables, but that would be a great contribution to the script that you could make! The script is written in a very straight-forward way, so should be quite simple to modify.
Specifically, you'll need to check for ElementTypes related to tables (TABLE, TABLE_ROW, TABLE_CELL) and then wrap those elements in the appropriate HTML tags. You can see how oazabir did it in this example:
if (item.getType() == DocumentApp.ElementType.PARAGRAPH) {
switch (item.getHeading()) {
// Add a # for each heading level. No break, so we accumulate the right number.
case DocumentApp.ParagraphHeading.HEADING6:
prefix = "<h6>", suffix = "</h6>"; break;
case DocumentApp.ParagraphHeading.HEADING5:
prefix = "<h5>", suffix = "</h5>"; break;
case DocumentApp.ParagraphHeading.HEADING4:
prefix = "<h4>", suffix = "</h4>"; break;
case DocumentApp.ParagraphHeading.HEADING3:
prefix = "<h3>", suffix = "</h3>"; break;
case DocumentApp.ParagraphHeading.HEADING2:
prefix = "<h2>", suffix = "</h2>"; break;
case DocumentApp.ParagraphHeading.HEADING1:
prefix = "<h1>", suffix = "</h1>"; break;
default:
prefix = "<p>", suffix = "</p>";
}
if (item.getNumChildren() == 0)
return "";
}
Once you've converted your template into HTML, then you can manipulate it using your existing code. (For example, have ConvertGoogleDocToCleanHtml() return the HTML text and save that into your mtxt variable.)
Lastly, in your current script, you make multiple calls to get the same range. These calls are really slow. Instead, try to get the range once, get the values, and then operate on the returned Array.
var mailMergeRange = SpreadsheetApp.openById("1Nc2xu--").getSheetByName("MailMerge List").getDataRange().getValues();
var numcols = mailMergeRange.length;
var numrows = mailMergeRange[0].length;
You could also try this approach using Advanced Drive Services and the MailChimp API.
Related
I have a Google Sheet with .gs script that is successfully generating dynamicnewRichTextValue() parameters which are meant to be injected into a Sheet cell that will contain multiple lines of text each with their own URL. I do not know all of the parameters in advance (might be one text and one link, or two each, or more) which is why I am dynamically generating the parameters.
Let's say the end-state should be this (in this case there are only two line items, but there could be more or less:
var RichTextValue=SpreadsheetApp.newRichTextValue()
.setText("mailto:fred#abcdef.com,mailto:jim#abcdef.com")
.setLinkUrl(0,6,"mailto:fred#abcdef.com")
.setLinkUrl(7,19,"mailto:jim#abcdef.com")
.build();
In my script I don't know how many "setText" parameters or "setLinkUrl" statements I will need to generate, so I am doing it dynamically.
This is simple to handle for "setText" because I can just pass a single variable constructed during an earlier loop that builds the "setText" parameters. Let's call that variable setTextContent, and it works like this:
var RichTextValue=SpreadsheetApp.newRichTextValue()
.setText(setTextContent)
So up to this point, everything is great. The problem is that I have another variable that generates the URL portion of the newrichtextvalue() parameters up to the ".build();" statement. So let's call that variable setUrlContent and it is built in an earlier loop and contains the string for the rest of the statement:
.setLinkURL(0,22,"mailto:fred#abcdef.com").setLinkURL(23,44,"mailto:jim#abcdef.com")
I am stumped trying to figure out how to attach it to the earlier bit. I feel like this is something simple I am forgetting. But I can't find it after much research. How do I hook up setUrlContent to the code above so that the command executes? I want to attach the bits above and get back to assigning it all to a variable I can put into a cell:
var emailCell=SpreadsheetApp.newRichTextValue()
.setText("mailto:fred#abcdef.com,mailto:jim#abcdef.com") // I can dynamically create up to here
.setLinkUrl(0,6,"mailto:fred#abcdef.com") // ...but these last couple lines are
.setLinkUrl(7,19,"mailto:jim#abcdef.com") // stuck in a string variable.
.build();
sheet.getRange(1,1,1,1).setRichTextValue(emailCell)
Thanks!
I believe your goal and situation as follows.
You want to use your script by dynamically changing the number of emails.
Modification points:
When your following script is run, I think that the links are reflected to mailto and fred#abcdef..
var emailCell=SpreadsheetApp.newRichTextValue()
.setText("mailto:fred#abcdef.com,mailto:jim#abcdef.com")
.setLinkUrl(0,6,"mailto:fred#abcdef.com")
.setLinkUrl(7,19,"mailto:jim#abcdef.com")
.build();
sheet.getRange(1,1,1,1).setRichTextValue(emailCell)
I thought that you might have wanted the linked email addresses like below.
fred#abcdef.com has the link of mailto:fred#abcdef.com.
jim#abcdef.com has the link of mailto:jim#abcdef.com.
In this answer, I would like to propose the modified script for above direction.
Modified script:
var inputText = "mailto:fred#abcdef.com,mailto:jim#abcdef.com"; // This is your sample text value.
var ar = inputText.split(",").map(e => {
var v = e.trim();
return [v.split(":")[1], v];
});
var text = ar.map(([e]) => e).join(",");
var emailCell = SpreadsheetApp.newRichTextValue().setText(text);
var start = 0;
ar.forEach(([t, u], i) => {
var len = t.length;
emailCell.setLinkUrl(start, start + len, u);
start += len + 1;
});
SpreadsheetApp.getActiveSheet().getRange(1,1,1,1).setRichTextValue(emailCell.build());
In this modification, inputText is splitted to the hyperlink and the text (for example, when your sample value is used, it's fred#abcdef.com and mailto:fred#abcdef.com.), and the text including the hyperlink are put to the cell.
In this case, for example, even when var inputText = "mailto:fred#abcdef.com,mailto:jim#abcdef.com" is modified to var inputText = "mailto:fred#abcdef.com" and var inputText = "mailto:fred#abcdef.com,mailto:jim#abcdef.com,mailto:sample#abcdef.com", each hyperlink are reflected to each text.
Note:
When you want to the hyperlink of mailto:fred#abcdef.com to the text of mailto:fred#abcdef.com, you can also use the following modified script.
var inputText = "mailto:fred#abcdef.com,mailto:jim#abcdef.com"; // This is your sample text value.
var ar = inputText.split(",").map(e => e.trim());
var emailCell = SpreadsheetApp.newRichTextValue().setText(inputText);
var start = 0;
ar.forEach((t, i) => {
var len = t.length;
emailCell.setLinkUrl(start, start + len, t);
start += len + 1;
});
SpreadsheetApp.getActiveSheet().getRange(1,1,1,1).setRichTextValue(emailCell.build());
References:
newRichTextValue()
Class RichTextValueBuilder
Class RichTextValue
I'm kinda new to coding and I'm trying to learn javascript by using it with google apps. I've figured out a few things trying to do my current project, but I seem to have gotten stuck.
I am attempting to create a program through google sheets that works sort of like a budgeting app, where I import data from my bank, run the program and it searches the data, changes the descriptions based on the content and adds a categorization.
I have figured out a good method for find and replace, but the problem I'm running into is that it only replaces part of the string instead of the whole thing. what i would like for it to do is search the data, if it finds a word that matches one of the cases i have entered, it will delete the entire string and replace it with a new description that I designate.
Here's what i have so far
function replaceStrings(){
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange("D1:D" + sheet.getLastRow());
var data = range.getValues();
for (var i = 0; i<data.length; i++) {
if (data[i][1] != "") {
switch (data[i][1]) {
case data.indexOf('MCDONALD\'S') >-1 :
data[i][1].replace(data[i][1],'McDonalds');
break;
}
}
}
range.setValues(data)
}
this is based off of a find and replace function that i had worked out, not sure if I can include the "data[i][0]" in a replace function like that but I have also tried deleting the string and appending the new text but that didn't work either.
When I run this I don't get any error messages, but nothing happens.
I don't know what your data looks so I used some of my own but this technique seems to work fine for me. I used getDataRange and my own indexes but you can modify whatever you need. Or if you wish you can share your spreadsheet and I can customize to your exact requirements.
function replaceStrings()
{
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getDataRange()
var data = range.getValues();
for (var i = 0; i<data.length; i++)
{
if(data[i][1].match(/firstMatch/))
{
data[i][1]='replacement1';
}
if(data[i][1].match(/secondMatch/))
{
data[i][1]='replacement2';
}
}
range.setValues(data);
}
Your can use anything you want for the match strings. I happen to use regular expressions and if you find a match you can replace the entire data element with anything you wish.
I am new to iMacro so please correct me if my approach with iMacro is incorrect.
I have created some javascript functions that are helpful in testing certain conditions on the DOM. The problem is i am not able to include javascript functions ( from External js file as well as defining on the .js script of imacro ) into my test file and call the functions from js lib during test case execution.
Have you tried using iimGetLastExtract()
You can create a .js file that calls imacros. those macros can extract information that you need from the DOM. using iimGetLastExtract() you can extract those values and test it in the js
when you are in FF you can actually choose a .js file to run from the imacros menu.
Here is an example of an old .js file i used to do just such a thing. im looping through the values of a dynamically created drop down and doing things based on its value. (if a macro fails it will a value less than 0: error codes )
var i = 1;
var notDone = 1;
while(notDone > 0) {
//select the board from available
iimSet('name', boardname);
iimPlay('selectBoard.iim');
//pick the next display view and capture the view name
iimSet('index',i);
notDone = iimPlay('assignDisplay.iim');
var displayName = iimGetLastExtract(1),
inputName = iimGetLastExtract(2),
label = '';
if((displayName == '[None]' && displayName == '[None]') || !notDone) {
break;
}
label = (displayName === '[None]') ? inputName : displayName;
if(prefix) label = prefix + label;
if(suffix) label += suffix;
iimSet('name', label);
iimSet('btn',btn);
iimPlay('assignLabelInput.iim');
i++;
}
If you arent familiar with some of the things in there. im using iimSet() to set variables in the imacros. iimPlay to play that macro. and in the macro itself, here is an example of extracting information
TAG POS=1 TYPE=SELECT FORM=NAME:form1 ATTR=ID:dropdownid EXTRACT=TXT
edit
Here is a silly example that will hopefully show the use of js and the imacros extract functionality
The code
example.js
var allAnswerVotes=[];
var runningTotal = 0;
var working = true;
var i = 2;
while(working) {
iimSet('i', i);
iimPlay('getVotes.iim');
var extract = iimGetLastExtract(1);
if(extract === '#EANF#') {
working = false;
continue; //hault this iteration;
}
var numVotes = parseInt(extract, 10);
allAnswerVotes.push(numVotes);
runningTotal += numVotes;
i++; //increment i to get the next vote
}
alert('The highest vote is '+ Math.max.apply(null, allAnswerVotes)+', with an average of '+ Math.ceil(runningTotal / allAnswerVotes.length));
getVotes.iim
TAG POS={{i}} TYPE=SPAN ATTR=class:*vote-count-post* EXTRACT=TXT
The explanation
First thing to do is make sure that both of these files (example.js, getVotes.iim) are located in the same folder in order for example.js to run getVotes.iim correctly.
next you just have to navigate to any StackOverflow thread, select example.js from the imacros menu (f8 to open the menu) and push play (or simply double click example.js)
the macro will find the i-th position span with class containing "vote-count-post" and return the text of that span. i is a parameter that is passed in by the js. we will start at i = 2, so the second vote span (we are skipping the vote for the question and only counting the votes for answers). the js will continue to call getVotes.iim until getVotes returns an extract value of "#EANF#" which is the return value when the macro cant find the specified tag (i.e. when there are no votes on the current page). #EANF# will kick us out of our loop and then you get the alert with some basic math on the votes we tallied.
The extracting of votes here is pretty silly but I'm just demonstrating a basic example of how to use imacros EXTRACT in js
I have a (GIS) project which displays large amounts of customer data (Thousands of records) to clients. Where nescessary/possible/required, we use server side pagination/filtering/data manipulation but there are cases where it is most efficient to send the data in JSON format to the client and let their browser do the filtering.
The amount of data is large, so we format it to save on bandwidth and parsing time - instead of individual objects, we send a structure that includes the attribute names first and then the values in a single flat array. On the client, we rebuild this into more traditional json objects before other processing occurs. eg:
{attrNames:["foo","bar"],values:[1,2,3,4,...]) -> [{foo:1,bar:2},{foo:3,bar:4},...]
The code for doing this looks a little like this:
function toObjectArray(attrNames, values){
var ret = [];
var index = 0;
var numAttrNames = attrNames.length;
var numValues = values.length;
while(index < numValues){
var obj = {};
for(var a = 0; a < numAttrNames; a++){
obj[attrNames[a]] = values[index++];
}
ret.push(obj);
}
return ret;
}
Given that the attributes may change depending on the customer data, is there a way to do this translation that takes advantage of hidden classes in modern javascript engines like V8? I have done some micro benchmarks similar to our use case ( http://jsfiddle.net/N6CrK/1/ ) where working with json such that hidden classes are used is orders of magnitude faster than building the objects as above. I can get some of this boost using "eval" to create objects, but this feels ugly (This is demonstrated in the js fiddle). Is there a better way? Perhaps using some variant of Object.create, or something like it?
You mean something like this right?
function toHiddenObjectArray(attrNames, attrValues){
var numAttrNames = attrNames.length,
numValues = attrValues.length;
function Data( values ) {
for(var v = 0; v < numAttrNames; v++) {
this[attrNames[v]] = values[v];
}
}
var ret=[];
for( var i=0; i<numValues ; i+=numAttrNames ) {
ret.push( new Data( attrValues.slice(i,i+numAttrNames) ) );
}
return ret;
}
You can check our the fiddle here: http://jsfiddle.net/B2Bfs/ (With some comparison code). It should use the same "Hidden Class" (i.e. Data). Not sure how much quicker it is though!
But, if you really want to make your code none blocking, why not load the page, then request the data via AJAX, then run all you code when you get a response.
I can get some of this boost using "eval" to create objects, but this feels ugly
There's a less ugly way using the Function constructor. Also, further optimisations can be done by immediately assigning the values to the properties, instead of initialising them with null and then again iterating through the attrs array like the adHoc does it. You'd just pass each of the rows you get in the response (array? string? byte-whatever?) as a parameter to the factory.
Also I've moved the creation of the factory function out of the create function, so that only one function will be instantiated (and optimized after enough calls to it).
A decent amount of the time in your test loop is spent on the getTotal, so I've optimised this in a similar manner. Not using getTotalAdHoc in testing the optimised solution drastically reduces the measured time (you can test with getTotalOptimum as well).
var factory = new Function("arr", "return{"+attrs.map(function(n, i){
return n+":arr["+i+"]";
}).join(",")+"};");
var getSum = new Function("o","return "+attrs.map(function(n, i){
return "o."+n;
}).join("+")+";");
(updated jsfiddle)
I haven't yet tried moving the complete loop into the generated code, which could avoid a few function calls, but I don't think this is necessary.
For some reason I just recalled this question... and I just came up with a solution that is way dirtier than using eval but which causes a huge speed boost. The downside of it is that code will be similarly little maintainable as when using eval.
The basic idea is: When receiving the attribute names, generate the function code to parse the following data in JavaScript and add it in a <script> tag to the <head>.
Yeah, isn't that dirty? :-)
If performance is so critical for you, it will definitely help you... here's a modified version of your microbenchmak that proves it: http://jsfiddle.net/N6CrK/17/
Some remarks on the code...
The two functions createWithGeneratedFunction and getTotalWithGeneratedFunction are simply wrapper functions that can be used by productive code. All they do is make sure that the <script> with the generated functions is set up and then call it.
function createWithGeneratedFunction(numValues){
makeSureScriptsAreSetUp()
return createWithGeneratedFunctionAdded(numValues);
}
function getTotalWithGeneratedFunction(objs){
makeSureScriptsAreSetUp()
return getTotalWithGeneratedFunctionAdded(objs);
}
The actual workhorse is the makeSureScriptsAreSetUp with the functions it creates. I'll go through it line by line:
function makeSureScriptsAreSetUp() {
if(scriptIsSetUp)
return;
If the required <script> tag was already set up this function will directly return since there is nothing to do for it anymore.
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
var theFunctions = "";
This prepares the creation of the required functions. The theFunctions variable will be filled with the code that is going to be put into the <script> tag content.
theFunctions =
"function createWithGeneratedFunctionAdded(numValues) {" +
" var ret = [];" +
" var value = 0;" +
" for(var i = numValues; i-- > 0;) {" +
" ret.push({";
for(var attr in attrs) {
theFunctions +=
" " + attrs[attr] + ": value++,";
}
theFunctions +=
" });" +
" }" +
" return ret;" +
"}" +
"";
This completes the code for the parsing function. Obviously it just "parses" the numbers 0 to numValues in this microbenchmark. But replacing value++ with something like TheObjectThatTheClientSentMe.values[value++] should bring you very close to what you outlined in your question. (Obviously it would make quite a lot of sense to rename value to index then.)
theFunctions +=
"function getTotalWithGeneratedFunctionAdded(objs) {" +
" var ret = 0;" +
" for(var i = objs.length; i-- > 0;) {" +
" var obj = objs[i];" +
" ret += 0";
for(var attr in attrs) {
theFunctions +=
" + obj." + attrs[attr];
}
theFunctions +=
" ;" +
" }" +
" return ret;" +
"}";
This completes the code for the processing function. Since you seem to require several processing functions, especially this code could become somewhat ugly to write and maintain.
script.text = theFunctions;
head.appendChild(script);
scriptIsSetUp = true;
}
In the very end we simply set the <script> tag content to the code we just created. By then adding that tag to the <head>, Chrome's hidden class magic will occur and will make the code VERY fast.
Concerning extensibility: If you have to query different attribute/value sets from the server on the same page, you might want to give each parsing/processing method set unique names. For example, if you first receive attrs = ["foo","bar"] and next attrs = ["foo","bar","baz"] you could concat the underscore-joined attribute name array to the generated function names.
For example, instead of using createWithGeneratedFunctionAdded you could use createWithGeneratedFunctionAdded_foo_bar for the first attribute/value set and createWithGeneratedFunctionAdded_foo_bar_baz for the second attribute/value set. An attr parameter could then be added to the wrapper functions that will be used to generate the correct code line for an eval (yes, here the evil eval would return) to trigger the correct generated function. Obviously, the attr parameter would also be required for the makeSureScriptsAreSetUp function.
I am currently trying to code an input form where you can type and format a text for later use as XML entries. In order to make the HTML code XML-readable, I have to replace the code brackets with the corresponding symbol codes, i.e. < with < and > with >.
The formatted text gets transferred as HTML code with the variable inputtext, so we have for example the text
The <b>Genji</b> and the <b>Heike</b> waged a long and bloody war.
which needs to get converted into
The <b>Genji</b> and the <b>Heike</b> waged a long and bloody war.
I tried it with the .replace() function:
inputxml = inputxml.replace("<", "<");
inputxml = inputxml.replace(">", ">");
But this would just replace the first occurrence of the brackets. I'm pretty sure I need some sort of loop for this; I also tried using the each() function from jQuery (a friend recommended I looked at the jQuery package), but I'm still new to coding in general and I have troubles getting this to work.
How would you code a loop which would replace the code brackets within a variable as described above?
Additional information
You are, of course, right in the assumption that this is part of something larger. I am a graduate student in Japanese studies and currently, I am trying to visualize information about Japenese history in a more accessible way. For this, I am using the Simile Timeline API developed by MIT grad students. You can see a working test of a timeline on my homepage.
The Simile Timeline uses an API based on AJAX and Javascript. If you don't want to install the AJAX engine on your own server, you can implement the timeline API from the MIT. The data for the timeline is usually provided either by one or several XML files or JSON files. In my case, I use XML files; you can have a look at the XML structure in this example.
Within the timeline, there are so-called "events" on which you can click in order to reveal additional information within an info bubble popup. The text within those info bubbles originates from the XML source file. Now, if you want to do some HTML formatting within the info bubbles, you cannot use code bracket because those will just be displayed as plain text. It works if you use the symbol codes instead of the plain brackets, however.
The content for the timeline will be written by people absolutely and totally not accustomed to codified markup, i.e. historians, art historians, sociologists, among them several persons of age 50 and older. I have tried to explain to them how they have to format the XML file if they want to create a timeline, but they occasionally slip up and get frustrated when the timeline doesn't load because they forgot to close a bracket or to include an apostrophe.
In order to make it easier, I have tried making an easy-to-use input form where you can enter all the information and format the text WYSIWYG style and then have it converted into XML code which you just have to copy and paste into the XML source file. Most of it works, though I am still struggling with the conversion of the text markup in the main text field.
The conversion of the code brackets into symbol code is the last thing I needed to get working in order to have a working input form.
look here:
http://www.bradino.com/javascript/string-replace/
just use this regex to replace all:
str = str.replace(/\</g,"<") //for <
str = str.replace(/\>/g,">") //for >
To store an arbitrary string in XML, use the native XML capabilities of the browser. It will be a hell of a lot simpler that way, plus you will never have to think about the edge cases again (for example attribute values that contain quotes or pointy brackets).
A tip to think of when working with XML: Do never ever ever build XML from strings by concatenation if there is any way to avoid it. You will get yourself into trouble that way. There are APIs to handle XML, use them.
Going from your code, I would suggest the following:
$(function() {
$("#addbutton").click(function() {
var eventXml = XmlCreate("<event/>");
var $event = $(eventXml);
$event.attr("title", $("#titlefield").val());
$event.attr("start", [$("#bmonth").val(), $("#bday").val(), $("#byear").val()].join(" "));
if (parseInt($("#eyear").val()) > 0) {
$event.attr("end", [$("#emonth").val(), $("#eday").val(), $("#eyear").val()].join(" "));
$event.attr("isDuration", "true");
} else {
$event.attr("isDuration", "false");
}
$event.text( tinyMCE.activeEditor.getContent() );
$("#outputtext").val( XmlSerialize(eventXml) );
});
});
// helper function to create an XML DOM Document
function XmlCreate(xmlString) {
var x;
if (typeof DOMParser === "function") {
var p = new DOMParser();
x = p.parseFromString(xmlString,"text/xml");
} else {
x = new ActiveXObject("Microsoft.XMLDOM");
x.async = false;
x.loadXML(xmlString);
}
return x.documentElement;
}
// helper function to turn an XML DOM Document into a string
function XmlSerialize(xml) {
var s;
if (typeof XMLSerializer === "function") {
var x = new XMLSerializer();
s = x.serializeToString(xml);
} else {
s = xml.xml;
}
return s
}
https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/replace
You might use a regular expression with the "g" (global match) flag.
var entities = {'<': '<', '>': '>'};
'<inputtext><anotherinputext>'.replace(
/[<>]/g, function (s) {
return entities[s];
}
);
You could also surround your XML entries with the following:
<![CDATA[...]]>
See example:
<xml>
<tag><![CDATA[The <b>Genji</b> and the <b>Heike</b> waged a long and bloody war.]]></tag>
</xml>
Wikipedia Article:
http://en.wikipedia.org/wiki/CDATA
What you really need, as mentioned in comments, is to XML-encode the string. If you absolutely want to do this is Javascript, have a look at the PHP.js function htmlentities.
I created a simple JS function to replace Greater Than and Less Than characters
Here is an example dirty string: < noreply#email.com >
Here is an example cleaned string: [ noreply#email.com ]
function RemoveGLthanChar(notes) {
var regex = /<[^>](.*?)>/g;
var strBlocks = notes.match(regex);
strBlocks.forEach(function (dirtyBlock) {
let cleanBlock = dirtyBlock.replace("<", "[").replace(">", "]");
notes = notes.replace(dirtyBlock, cleanBlock);
});
return notes;
}
Call it using
$('#form1').submit(function (e) {
e.preventDefault();
var dirtyBlock = $("#comments").val();
var cleanedBlock = RemoveGLthanChar(dirtyBlock);
$("#comments").val(cleanedBlock);
this.submit();
});