在學習html5的時候,使用canvas實現了對html文本的解析和渲染,支持的tag有<p>、<i>、<b>、<u>、<ul>、<li>,並參考chrome對不規則html進行了解析。代碼扔在了個人github上(https://github.com/myhonor2013/gadgets,裏面的html-render-with-canvas目錄裏面)。html
程序的主函數是個循環,對html文本從左往右進行解析,wangy每一個循環開始處指向有效的html '<'位置,例如'<p'、'<\'、'<f'、'<\x'等都是有效的,而'< p'、'< \'、'< \x'等都是無效的,總之'<'後面必須緊跟一個非空字符,這是參考了chrome的解析得出的結論。這也就意味着每一個循環的末尾必須找到第一個相似的位置才能結束循環。在每次循環末尾調用渲染函數在canvas上進行渲染。在循環的過程當中要時刻注意html字符串指針是否越界,若是越界則結束循環進行渲染。html5
1、預處理git
預處理簡單地對html文本中連續的空字符(回車、tab、縮進)用單個的空格進行了替換:github
var text=data.text.replace(/[\r\n\t]/g,WHITESPACE).replace(/\s+/g,WHITESPACE).trim();
而後從html文本開始位置尋找第一個所謂的有效tag位置,並對此位置以前的文本進行渲染。如下則每次循環都以有效的'<'開始。這又分兩種狀況:有效開標籤和有效閉標籤。正則表達式
2、有效開標籤的處理chrome
有效開標籤即'<'後面不是'\'的標籤,用正則表達式就是^<[^\/]+.*。尋找和'<'匹配的'>'標籤並將標籤名稱push到tagname中。接下來根據tagname肯定其後面的文本應該採用的格式,亦即isbold、isitalic、isicon(<li>標籤)、isunderline、nowrap、uloffset等屬性,並進而根據isbold和isitalic肯定繪製canvas須要的font屬性值。font和isicon、isunderline、nowrap、uloffset即是canvas渲染真正須要的屬性。若是是支持的tag同時將標籤名稱push到tagnames,將font 入棧到fontsarr中,後面的循環要根據這兩個屬性來肯定其做用域的文本格式。canvas
1 while(text[index]!=WHITESPACE&&text[index]!=RIGHTSYN){ 2 tagname.push(text[index++]); 3 if(index==len)break; 4 } 5 if(index==len)return; 6 while(text[index]!=RIGHTSYN){ 7 if(index==len){ 8 break; 9 } 10 } 11 var tag=tagname.join('').toLowerCase(); 12 tagname=[]; 13 if(tag==TAGB){ 14 isbold=true; 15 } 16 else if(tag==TAGI){ 17 isitalic=true; 18 } 19 else if(tag==TAGLI){ 20 isicon=true; 21 } 22 else if(tag==TAGU){ 23 isunderline=true; 24 } 25 if(tag==TAGP||tag==TAGLI||tag==TAGUL){ 26 nowrap=false; 27 } 28 else{ 29 nowrap=true; 30 } 31 if(tag==TAGUL){ 32 uloffset+=ULOFFSET; 33 } 34 35 if(isitalic==true&&isbold==true){ 36 font=ITALICBOLD; 37 } 38 else if(isitalic==false&&isbold==true){ 39 font=BOLD; 40 } 41 else if(isitalic==true&&isbold==false){ 42 font=ITALIC; 43 } 44 else{ 45 font=NORMAL; 46 } 47 if(VALIDTAGS.contains(tag)){ 48 tagnames.push(tag); 49 fontsarr.push(font); 50 }
後面部分就是本次循環的做用域文本,文本被放在texttodraw中並在結束前進行canvas渲染。在結束前還要將texttodraw清空,並將isicon置爲false。函數
3、有效閉標籤的處理性能
有效閉標籤即'<'後面緊跟'\'的標籤,用正則表達式就是^<\/.*。一樣往前找出其匹配的閉合'<'。若是閉合標籤名和tagnames(其中依次保存了有效開標籤處理時的標籤名稱,還記得嗎)中的最後一個相同,則將tagnames的最後一個元素出棧。若是標籤名稱是ul則對uloffset往前縮進;若是tagnames中再也不包含當前標籤名稱,則根據標籤語義對字體進行相應處理,這是考慮了多層嵌套的狀況。學習
1 if(text[index]=="/"){ 2 var arr=[]; 3 while(++index<len&&text[index]!=RIGHTSYN&&text[index]!=LEFTSYN){ 4 arr.push(text[index]); 5 } 6 if(index==len)return; 7 if(text[index]==LEFTSYN)break; 8 var tag=arr.join('').trim().toLowerCase(); 9 if(tag==tagnames[tagnames.length-1]){ 10 font=fontsarr.pop(); 11 tagnames.pop(); 12 if(tag==TAGUL){ 13 uloffset -=ULOFFSET; 14 uloffset =(uloffset>0)?uloffset:0; 15 } 16 if(!tagnames.contains(tag)){ 17 if(tag==TAGI){ 18 font=font.replace("italic",'normal'); 19 isitalic=false; 20 } 21 else if(tag==TAGB){ 22 font=font.replace("bold",'normal'); 23 isbold=false; 24 } 25 else if(tag==TAGU){ 26 isunderline=false; 27 } 28 } 29 } 30 }
接下來一樣是本次循環的做用域文本,對其進行獲取並根據前面肯定的屬性值對其進行渲染。和開標籤的處理一致,再也不贅述。
4、canvas渲染
兩個全局變量xoffset和yoffset用以標識上次渲染結束後的位置。在渲染開始時首先對這兩個屬性須要根據uloffset、nowrap等屬性進行調整。而後若是具備isicon屬性,則繪製出<li>標籤對應的前面的實心圓。接着就是對文本進行渲染了,設定font後逐字符取出並使用measureText測量是否滿行,若是是則繪製後須要換行。在渲染過程當中若是須要繪製下劃線則一併進行繪製。如此反覆,直到全部字符繪製完畢。完整的渲染函數以下:
1 var drawtext=function(data){ 2 data=data.trim(); 3 var len=data.length; 4 if(len==0){ 5 return; 6 } 7 if(!nowrap&&xoffset>MARGIN){ 8 xoffset = MARGIN+uloffset; 9 yoffset += LINEHEIGHT; 10 } 11 12 if(isicon){ 13 ctx.beginPath(); 14 ctx.arc(MARGIN+uloffset+MARGIN,yoffset-MARGIN,MARGIN,0,Math.PI*2,true); 15 ctx.closePath(); 16 ctx.fill(); 17 xoffset +=30; 18 } 19 20 21 var index=0; 22 var renderindex=0; 23 ctx.font=font; 24 while(index<len){ 25 while(canvaswidth-xoffset>ctx.measureText(data.substring(renderindex,++index)).width){ 26 if(index===len){ 27 break; 28 } 29 } 30 31 if(index==len){ 32 ctx.fillText(data.substring(renderindex,index),xoffset,yoffset); 33 if(isunderline){ 34 canvas.strokeStyle = "red"; 35 canvas.lineWidth = 5; 36 ctx.beginPath(); 37 ctx.moveTo(xoffset, yoffset); 38 ctx.lineTo(xoffset+ctx.measureText(data.substring(renderindex,index)).width, yoffset); 39 ctx.closePath(); 40 ctx.stroke(); 41 } 42 xoffset+=ctx.measureText(data.substring(renderindex,index)).width; 43 break; 44 } 45 ctx.fillText(data.substring(renderindex,--index),xoffset,yoffset); 46 if(isunderline){ 47 canvas.strokeStyle = "red"; 48 canvas.lineWidth = 5; 49 ctx.beginPath(); 50 ctx.moveTo(xoffset, yoffset); 51 ctx.lineTo(canvaswidth, yoffset); 52 ctx.closePath(); 53 ctx.stroke(); 54 } 55 56 57 renderindex=index; 58 xoffset = MARGIN; 59 yoffset += LINEHEIGHT; 60 } 61 return; 62 };
結束語
使用js解析html時切忌使用遞歸,這樣處理很容易形成堆棧溢出和性能問題。另代碼中出現的Array的contains方法是在Array的prototype上添加的用以判斷是否包含字符串的方法:
Array.prototype.contains=function(item){
return new RegExp("^" + this.join("|")+ "$","i").test(item.toString());
}