Created
February 10, 2017 16:17
-
-
Save sethleedy/3bea5e16bbc1f5b4fe8a64bb492b0901 to your computer and use it in GitHub Desktop.
A lot of utilities combined by Patrick Horgan patrick at dbp-consulting dot com
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
// Copyright Patrick Horgan patrick at dbp-consulting dot com | |
// Permission to use granted as long as you keep this notice intact | |
// use strict is everywhere because some browsers still don't support | |
// using it once for the whole file and need per method/function | |
// use. | |
// Parts are derivitive of work by Jeff Walden, Michael Kuhn | |
// and Gavin Kistner as noted below as appropriate. | |
function colorObject(color) | |
// color | |
// undefined for black opaque, | |
// #fec or #f337ae for hex rep of rgb | |
// rgb like rgb(217,3,14) | |
// rgba like rgba(217,3,14,.42) | |
// hsl like hsl(30, 17, 14) | |
// hsla like hsl(30, 17, 14, .3) | |
// color like maroon or navy, one of the 16 original http colors | |
// plus orange and transparent (rgb(0,0,0,0)) | |
{ var colorNames=[ | |
['aqua','rgb(0,255,255)'], | |
['black','rgb(0,0,0)'], | |
['blue','rgb(0,0,255)'], | |
['fuchsia','rgb(255,0,255)'], | |
['gray','rgb(128,128,128)'], | |
['green','rgb(0,128,0)'], | |
['lime','rgb(0,255,0)'], | |
['maroon','rgb(128,0,0)'], | |
['navy','rgb(0,0,128)'], | |
['olive','rgb(128,128,0)'], | |
['purple','rgb(128,0,128)'], | |
['red','rgb(255,0,0)'], | |
['silver','rgb(192,192,192)'], | |
['teal','rgb(0,128,128)'], | |
['transparent','rgba(0,0,0,0)'], | |
['white','rgb(255,255,255)'], | |
['yellow','rgb(255,255,0)'], | |
['orange','rgb(255,166,0)'] | |
]; | |
var self=this; | |
this.origval=color; | |
this.colors=[0,0,0]; | |
this.alpha=1.0; | |
this.valid=true; | |
this.setrgb=0; | |
var hueToRGB=function(m1,m2,h) | |
{ | |
h=h<0?h+1:h>1?h-1:h; | |
if(h*6<1){ | |
return (m1+(m2-m1)*h*6)*255; | |
}else if(h*2<1){ | |
return m2*255; | |
}else if(h*3<2){ | |
return (m1+(m2-m1)*(2/3-h)*6)*255; | |
}else{ | |
return m1*255; | |
} | |
} | |
// parseColor - only called with an argument that has already matched a | |
// regular expression as a number or number% (for rgb or rgba) | |
var parseColor=function(acolor) | |
{ | |
var color; | |
if(/\d*%/.test(acolor)){ // is in the form 83% | |
color=Math.round(255*parseInt(acolor.substr(0,acolor.length-1))/100); | |
}else{ | |
color=parseInt(acolor); | |
} | |
if( color<0 || color>255){ | |
this.valid=false; | |
} | |
return color<0?0:color>255?255:color; | |
} | |
// color is a string like #f38 or #f3c722 or rgb(0,0,7), etc. | |
//console.log("type of color: "+typeof(color)); | |
if(typeof(color)==='undefined'){ | |
this.colors[0]=0; | |
this.colors[1]=0; | |
this.colors[2]=0; | |
this.valid=false; | |
} else if(color.substr(0,1)==='#'){ | |
var numlength=2; | |
if(color.length==4){ | |
numlength=1; | |
} | |
// like #fc3 which should be same as #ffcc33 | |
var aclr; | |
var i; | |
for(i=0;i<3;i++){ | |
aclr=parseInt(color.substr(numlength*i+1,numlength),16); | |
if(isNaN(aclr)){ | |
// this looks like a good test but can miss problems with the | |
// two digit hex nums. ek will parse a 14 with no error, but | |
// obviously it's not valid. Sigh. | |
this.valid=false; // Default to the zero value, but flag error | |
}else if(numlength==1){ | |
// set single digit color to value as if it were two digit hex | |
// with both digits the same | |
this.colors[i]=aclr*16+aclr; | |
}else{ | |
// 2 digit hex | |
this.colors[i]=aclr; | |
} | |
} | |
}else if(color.substr(0,4)==='rgba'){ | |
var digits = /rgba\(\s*(\d+%?)\s*,\s*(\d+%?)\s*,\s*(\d+%?)\s*,\s*([+-]?\d*.?\d*)\s*\)/.exec(color); | |
if(digits===null){ | |
this.valid=false; | |
return; | |
} | |
// parseColor will set valid to false if numbers not in range 0-255 | |
// but will clamp negative to 0 and >255 to 255. | |
this.colors[0]=parseColor(digits[1]); | |
this.colors[1]=parseColor(digits[2]); | |
this.colors[2]=parseColor(digits[3]); | |
this.alpha=parseFloat(digits[4]); | |
if(isNaN(this.alpha)){ | |
this.valid=false; | |
this.alpha=1; | |
}else if(this.alpha<0 || this.alpha>1){ | |
// they'll be clamped to range 0-1, but flag as invalid | |
this.valid=false; | |
} | |
this.alpha=this.alpha<0?0:this.alpha>1?1.0:this.alpha; | |
}else if(color.substr(0,3)==='rgb'){ | |
var digits = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\s*\)/.exec(color); | |
if(digits===null){ | |
this.valid=false; | |
return; | |
} | |
// parseColor will set valid to false if numbers not in range 0-255 | |
// but will clamp negative to 0 and >255 to 255. | |
this.colors[0]=parseColor(digits[1]); | |
this.colors[1]=parseColor(digits[2]); | |
this.colors[2]=parseColor(digits[3]); | |
}else if(color.substr(0,4)==='hsla'){ | |
var digits = /hsla\(\s*(\d+)\s*,\s*(\d+)\s*%\s*,\s*(\d+)\s*%\s*\s*,\s*(\d*.?\d+)\)/.exec(color); | |
if(digits===null){ | |
this.valid=false; | |
return; | |
} | |
// hsl in css is hsl(hue_angle,saturation%,lightness%) | |
// We want the angle normalized to [0-359], and the saturation and | |
// lightness converted from percentages to numbers [0-1]. This is | |
// so we can use a standard algorithm to convert to rgb to store in | |
// colors[0-2] | |
// this strange thing normalizes in range [0,1) | |
// first mod 360 puts in range [-359,359] | |
// then we add 360 to put in range [0,719] | |
// second mod 360 puts in range [0,359] | |
// then division puts in range [0,.99722222] (approximately) | |
var hue=(((parseInt(hue)%360)+360)%360)/360; // normalize 0-1 | |
// Saturation is a percentage, we convert to a number [0-1] | |
var saturation=parseInt(digits[2])/100; | |
var m1,m2; | |
if(saturation<0 || saturation>1){ | |
this.valid=false; | |
} | |
saturation=saturation<0?0:saturation>1?1:saturation; | |
// lightness is a percentage, we convert to a number [0-1] | |
var lightness=parseInt(digits[3])/100; | |
if(lightness<0 || lightness>1){ | |
this.valid=false; | |
} | |
lightness=lightness<0?0:lightness>1?1:lightness; | |
if(lightness<.5){ | |
m2=lightness*(saturation+1); | |
}else{ | |
m2=lightness+saturation-lightness*saturation; | |
} | |
m1=lightness*2-m2; | |
this.colors[0]=hueToRGB(m1,m2,hue+1/3); | |
this.colors[1]=hueToRGB(m1,m2,hue); | |
this.colors[2]=hueToRGB(m1,m2,hue-1/3); | |
this.alpha=digits[4]<0?0:digits[4]>1?1:digits[4]; | |
}else if(color.substr(0,3)==='hsl'){ | |
var digits = /hsl\(\s*(\d+)\s*,\s*(\d+)\s*%\s*,\s*(\d+)\s*%\s*\s*\)/.exec(color); | |
if(digits===null){ | |
this.valid=false; | |
return; | |
} | |
var hue=(((digits[1]%360)+360)%360)/360; // normalize 0-1 | |
var saturation=digits[2]/100; | |
var m1,m2; | |
saturation=saturation<0?0:saturation>1?1:saturation; | |
var lightness=digits[3]/100; | |
lightness=lightness<0?0:lightness>1?1:lightness; | |
if(lightness<.5){ | |
m2=lightness*(saturation+1); | |
}else{ | |
m2=lightness+saturation-lightness*saturation; | |
} | |
m1=lightness*2-m2; | |
this.colors[0]=hueToRGB(m1,m2,hue+1/3); | |
this.colors[1]=hueToRGB(m1,m2,hue); | |
this.colors[2]=hueToRGB(m1,m2,hue-1/3); | |
}else{ | |
for(var ctr=0;ctr<colorNames.length;ctr++){ | |
if(color==colorNames[ctr][0]){ | |
var digits = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\s*\)/.exec(colorNames[ctr][1]); | |
this.colors[0]=parseColor(digits[1]); | |
this.colors[1]=parseColor(digits[2]); | |
this.colors[2]=parseColor(digits[3]); | |
break; | |
} | |
} | |
} | |
this.red=function(){ return this.colors[0]; }; | |
this.green=function(){ return this.colors[1]; }; | |
this.blue=function(){ return this.colors[2]; }; | |
this.tohexStr=function() | |
{ | |
return '#'+ this.colors[0].toString(16)+this.colors[1].toString(16)+this.colors[2].toString(16); | |
}; | |
this.torgbStr=function() | |
{ | |
return 'rgba('+this.colors[0]+','+this.colors[1]+','+this.colors[2]+','+this.alpha+')'; | |
} | |
this.tohslStr=function() | |
{ | |
var r=this.colors[0]/255; | |
var g=this.colors[1]/255; | |
var b=this.colors[2]/255; | |
var max=Math.max(r,g,b),min=Math.min(r,g,b); | |
var h, s, l = (max + min) / 2; | |
if(max == min){ | |
h = s = 0; // achromatic | |
}else{ | |
var d = max - min; | |
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); | |
switch(max){ | |
case r: h = (g - b) / d + (g < b ? 6 : 0); break; | |
case g: h = (b - r) / d + 2; break; | |
case b: h = (r - g) / d + 4; break; | |
} | |
h /= 6; | |
} | |
return "hsla(" | |
+Math.floor(h*360)+"," | |
+Math.floor(s*100)+"," | |
+Math.floor(l*100)+"," | |
+this.alpha+")"; | |
} | |
this.toString=function(){ return "colorObject('"+this.origval+"')"; }; | |
//this.toString=function(){ return "colorObject("+this.torgbStr()+")"; }; | |
this.mult=function(val) | |
{ | |
val=val<0?0:val; | |
return 'rgba('+Math.min(255,Math.round(this.colors[0]*val))+',' | |
+Math.min(255,Math.round(this.colors[1]*val))+',' | |
+Math.min(255,Math.round(this.colors[2]*val))+','+this.alpha+')'; | |
} | |
this.gettransp=function(val) | |
{ | |
val=val<0?0:val>1?1:val; | |
return 'rgba('+this.colors[0]+','+this.colors[1]+','+this.colors[2] | |
+','+val+')'; | |
} | |
} | |
function backgroundFade(obj,fromColor,toColor,ms,steps) | |
{ | |
var self=this; | |
var curInterval=0; | |
var fades=new Array(); | |
var which=0; | |
var onefade=function(obj,fromColor,toColor,ms,steps){ | |
var self=this; | |
var theobj=obj; | |
var from=fromColor; | |
var to=toColor; | |
var len=ms; | |
var numsteps=steps; | |
var step=ms/steps; | |
var redstep= (to.red()-from.red())/numsteps; | |
var greenstep= (to.green()-from.green())/numsteps; | |
var bluestep= (to.blue()-from.blue())/numsteps; | |
var curstep=0; | |
var ondone; | |
this.obj=function(){ return theobj; }; | |
this.from=function(){ return from; }; | |
this.to=function(){ return to; }; | |
this.len=function(){ return len; }; | |
this.astep=function() { return step; }; | |
this.numsteps=function(){ return numsteps; }; | |
this.redstep=function(){ return redstep; }; | |
this.greenstep=function(){ return greenstep; }; | |
this.bluestep=function(){ return bluestep; }; | |
this.curstep=function(){ return curstep; }; | |
this.bumpstep=function(){ curstep+=1 }; | |
this.resetstep=function(){ curstep=0 }; | |
this.toString=function(){ | |
return "\ttheobj: "+theobj+"\n\tfrom: "+from+", to: "+to+"\n\tlen: "+len+", numsteps: "+numsteps+"\n\tredstep: "+redstep+", greenstep: "+greenstep+", bluestep: "+bluestep+"\n\tcurstep: "+curstep; | |
} | |
this.callb=function() | |
{ | |
var red=Math.round(from.red()+redstep*curstep); | |
var green=Math.round(from.green()+greenstep*curstep); | |
var blue=Math.round(from.blue()+bluestep*curstep); | |
theobj.style.backgroundColor="rgb("+red+","+green+","+blue+")"; | |
if(curstep<numsteps){ | |
curstep++; | |
setTimeout(self.callb,step,self); | |
}else{ | |
theobj.style.backgroundColor="rgb("+to.red()+","+to.green()+","+to.blue()+")"; | |
ondone(); | |
} | |
} | |
this.run=function(callb) | |
{ | |
ondone=callb; | |
setTimeout(self.callb,step,self); | |
} | |
} | |
var done=function(){ | |
which++; | |
if(which<fades.length){ | |
self.run(); | |
} | |
} | |
this.run=function() | |
{ | |
if(which<fades.length){ | |
fades[which].run(done); | |
} | |
} | |
// push_fade checks the inputs and pushes one fade request on the queue | |
this.push_fade=function(obj,fromColor,toColor,ms,steps) | |
{ | |
var theobj; | |
if(typeof(obj)==="object"){ | |
theobj=obj; | |
}else if(typeof(obj)=="string"){ | |
theobj=document.getElementById(obj); | |
}else{ | |
return; | |
} | |
if(typeof(theobj)==="object") { | |
fades.push( new onefade( theobj, fromColor, toColor,ms,steps)); | |
} | |
} | |
self.push_fade(obj,fromColor,toColor,ms,steps); | |
} | |
function isNumber(n) | |
{ | |
// like to ascribe this to someone, but see it all over the net. | |
'use strict'; | |
return !isNaN(parseFloat(n)) && isFinite(n); | |
} | |
// getCookies - returns a hash of cookies | |
function getCookies() | |
{ | |
var rawcookies=document.cookie.split(";"); | |
var cookies=new Array; | |
for(var cookie in rawcookies){ | |
var cookiepair=cookie.split("="); | |
cookies[cookiepair[0]]=cookiepair[1]; | |
} | |
return cookies; | |
} | |
// Array Remove - By Jeff Walden | |
Array.prototype.remove = function(from, to) | |
{ | |
// Array Remove - By Jeff Walden | |
'use strict'; | |
this.splice(from, | |
!to || | |
1 + to - from + (!(to < 0 ^ from >= 0) && (to < 0 || -1) * this.length)); | |
return this.length; | |
}; | |
function hookEvent(element, eventName, callback) | |
{ | |
// from Michael Kuehl as used in an article: | |
// http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel | |
'use strict'; | |
if(typeof(element) == "string"){ | |
element = document.getElementById(element); | |
} | |
if(element == null){ | |
return; | |
} | |
if(element.addEventListener) { | |
if(eventName == 'mousewheel'){ | |
element.addEventListener('DOMMouseScroll', callback, false); | |
} | |
element.addEventListener(eventName, callback, false); | |
} else if(element.attachEvent){ | |
element.attachEvent("on" + eventName, callback); | |
} | |
} | |
function unhookEvent(element, eventName, callback) | |
{ | |
// from Michael Kuehl as used in an article: | |
// http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel | |
'use strict'; | |
if(typeof(element) == "string"){ | |
element = document.getElementById(element); | |
} | |
if(element == null){ | |
return; | |
} | |
if(element.removeEventListener) { | |
if(eventName == 'mousewheel'){ | |
element.removeEventListener('DOMMouseScroll', callback, false); | |
} | |
element.removeEventListener(eventName, callback, false); | |
} else if(element.detachEvent){ | |
element.detachEvent("on" + eventName, callback); | |
} | |
} | |
function stopListening(eventTarget,eventType,eventHandler) | |
{ | |
// from Michael Kuehl as used in an article: | |
// http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel | |
'use strict'; | |
if(eventTarget.removeEventListener){ | |
eventTarget.removeEventListener(eventType,eventHandler,false); | |
}else if(eventTarget.detachEvent){ | |
eventType = 'on'+eventType; | |
eventTarget.detachEvent(eventType,eventHandler); | |
}else{ | |
eventTarget['on'+eventType]=null; | |
} | |
} | |
// from Michael Kuehl as used in an article: | |
// http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel | |
function cancelEvent(e) | |
{ | |
'use strict'; | |
if(!e){e=window.event;} | |
if(e.stopPropagation){e.stopPropagation();} | |
if(e.preventDefault) {e.preventDefault(); } | |
e.cancelBubble = true; | |
e.cancel = true; | |
e.returnValue = false; | |
return false; | |
} | |
// this routine from Gavin Kistner from this article: | |
// http://stackoverflow.com/questions/5527601/normalizing-mousewheel-speed-across-browsers | |
var wheelDistance = function(e) | |
{ | |
'use strict'; | |
if (!e) e = window.event; | |
var w=e.wheelDelta, d=e.detail; | |
if (d){ | |
if (w){ | |
return w/d/40*d>0?1:-1; // Opera | |
} else{ | |
return -d/3; // Firefox; TODO: do not /3 for OS X | |
} | |
} else{ | |
return w/120; // IE/Safari/Chrome TODO: /3 for Chrome OS X | |
} | |
} | |
var wheelDirection = function(e){ | |
// this routine from Gavin Kistner from this article: | |
// http://stackoverflow.com/questions/5527601/normalizing-mousewheel-speed-across-browsers | |
'use strict'; | |
if (!e) e = window.event; | |
return (e.detail<0) ? 1 : (e.wheelDelta>0) ? 1 : -1; | |
}; | |
function getStyleObj(obj,styleProp) | |
{ | |
// Got this from David Cramer | |
// http://davidcramer.posterous.com/code/84/get-offsets-xy-for-an-object-javascript.html | |
if (obj.currentStyle){ | |
var s = obj.currentStyle[styleProp]; | |
} else if (window.getComputedStyle){ | |
var s = document.defaultView.getComputedStyle(obj,null).getPropertyValue(styleProp); | |
} | |
return s; | |
} | |
function getStyleID(el,styleProp) | |
{ | |
var obj = document.getElementById(el); | |
return getStyleObj(obj, styleProp); | |
} | |
var getTextHeight = function(font,fontsize,fontweight,fontstyle,fontvariant,thetext) | |
{ | |
var result={}; | |
var blockdiv=document.createElement("div"); | |
blockdiv.style.display="inline-block"; | |
blockdiv.style.width="1px"; | |
blockdiv.style.height="0px"; | |
var body=document.getElementsByTagName("body")[0]; | |
var aspan=document.createElement("span"); | |
if(typeof font === 'undefined'){ | |
return result; | |
} | |
aspan.style.fontFamily=font; | |
if(typeof fontsize !== 'undefined'){ | |
aspan.style.fontSize=fontsize; | |
} | |
if(typeof fontweight !== 'undefined'){ | |
aspan.style.fontWeight=fontweight; | |
} | |
if(typeof fontstyle !== 'undefined'){ | |
aspan.style.fontStyle=fontstyle; | |
} | |
if(typeof fontvariant !== 'undefined'){ | |
aspan.style.fontVariant=fontvariant; | |
} | |
if(typeof thetext !== 'undefined'){ | |
aspan.innerHTML=thetext; | |
}else{ | |
aspan.innerHTML="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |
} | |
var adiv=document.createElement("div"); | |
adiv.appendChild(aspan); | |
adiv.appendChild(blockdiv); | |
var body=document.getElementsByTagName('body')[0] | |
try{ | |
body.appendChild(adiv); | |
blockdiv.style.verticalAlign="baseline"; | |
result.ascent=blockdiv.offsetTop - aspan.offsetTop; | |
blockdiv.style.verticalAlign="bottom"; | |
result.height=blockdiv.offsetTop - aspan.offsetTop; | |
result.descent=result.height-result.ascent; | |
}finally{ | |
body.removeChild(adiv); | |
} | |
return result; | |
}; | |
function toc_generator() | |
{ | |
var self=this; | |
var toc_clicktoshow='<strong>Click to show Table of Contents</strong>'; | |
var toc_clicktohide='<strong>Click to hide Table of Contents</strong>'; | |
var toc=document.getElementById('toc'); | |
if(!toc){ | |
return null; | |
} | |
var toc_toggle_target; | |
var toc_box; | |
var toggleTOCVisibility=function() | |
{ | |
if(toc_toggle_target.innerHTML==toc_clicktoshow){ | |
toc_toggle_target.innerHTML=toc_clicktohide; | |
toc_box.hidden=false; | |
toc_box.style.display='block'; | |
}else{ | |
toc_toggle_target.innerHTML=toc_clicktoshow; | |
toc_box.hidden=true; | |
toc_box.style.display='none'; | |
} | |
} | |
var getHeaders=function() | |
{ | |
var headers=new Array(); | |
var hdr_names=[ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ]; | |
for(var i=0;i<hdr_names.length;i++){ | |
var elems=document.getElementsByTagName(hdr_names[i]); | |
for(var j=0;j<elems.length;j++){ | |
headers.push(elems[j]); | |
} | |
} | |
// Now sort them | |
// I learned how to do this from Peter-Paul Koch from here: | |
// http://www.quirksmode.org/dom/getElementsByTagNames.html | |
if (headers[0].sourceIndex) { | |
headers.sort(function (a,b) { return a.sourceIndex - b.sourceIndex; }); | |
} else if (headers[0].compareDocumentPosition){ | |
headers.sort(function (a,b) { return 3 - (a.compareDocumentPosition(b) & 6); }); | |
} | |
return headers; | |
} | |
this.genTOC=function() | |
{ | |
if(!toc){ | |
return; | |
} | |
toc.onclick=toggleTOCVisibility; | |
toc_toggle_target=toc.appendChild(document.createElement('span')); | |
toc_toggle_target.id='toc_toggle_target'; | |
toc_toggle_target.innerHTML=toc_clicktoshow; | |
toc_box=toc.appendChild(document.createElement('div')); | |
toc_box.id='toc_box'; | |
toc_box.hidden=true; | |
toc_box.style.display='none'; | |
toc_box.appendChild(document.createElement('hr')); // hr just to separate the toggle target | |
self.headers=getHeaders(); // get all headers in order | |
// below, /\bnotoc\b/ is a literal regular expression. It's compiled at | |
// the time the script is instantiated rather than when it's run. | |
// The regular expression is \bnotoc\b. \b means word boundary | |
// so it would match classes like 'foo notoc' or 'notoc foo' | |
// but not 'foonotoc' or 'notocfoo' | |
for(var i=0;i<self.headers.length;i++){ | |
if(!/\bnotoc\b/.test(self.headers[i].className)){ | |
var toc_line=toc_box.appendChild(document.createElement('a')); | |
toc_line.innerHTML=self.headers[i].innerHTML; | |
toc_line.className='toc_class'+self.headers[i].nodeName; | |
if(!self.headers[i].id){ | |
self.headers[i].id='toc_link'+i; | |
} | |
toc_line.href='#'+self.headers[i].id; | |
} | |
} | |
delete self.headers; | |
self.headers=null; // Done with them | |
} | |
} | |
this.toc=new toc_generator(); | |
if(this.toc.genTOC){ | |
this.toc.genTOC(); // Automagically try to generate toc. | |
} | |
delete this.toc; | |
this.toc=null; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment