為在網頁中插入「空格」編寫的JS指令碼

高翌翔發表於2012-08-08

昨天李鬆峰老師在微博中寫道:

程式設計師一般特別在意一個細節:中文與西文、中文與數字之間要有間距。這樣寫:HTML5定義了29個新標籤。他們覺得不爽,改成這樣:HTML5_定義了_29_個新標籤。就舒服了。如果你在翻譯書,千萬不要人為敲這麼多半字空。因為排版軟體一般都有設定選項,你加了,別人再刪很麻煩。

Microsoft Word-段落-中文版式

圖1:Microsoft Word-段落-中文版式

【注】:上文中三個下劃線的位置原為空格,這樣處理只是為突出顯示空格,因為本文及相關程式碼都與這些空格有著不解之緣 :D

接著俺回覆:

確實,在Microsoft Word中可以很方便地控制版式。不過在HTML程式碼中,要想控制“中文與西文、中文與數字之間的間距”就不那麼方便了,請問李老師有啥好辦法沒?現在能想到的是JS+CSS,這樣原文會比較乾淨。

接下來是一段微博對話,正是這段對話引出了最後的程式碼,急性子的童鞋可跳過對話,直接看末尾的格式化程式碼就可以了。

【李】JS+CSS要是能控制就可以呀,可是怎麼控制呢?
【俺】初步想法是,用正則掃描正文文字,插入樣式標籤,即處理前“HTML5定義了29個新標籤”,處理後“<span class='gap'>HTML5</span>定義了<span class='gap'>29</span>個新標籤”。
【李】能看懂這個正則嗎:“([!-~]) ([^!-~])”為“$1$2” ,這是別人寫的。
【俺】兩個小括號是模式匹配,[!-~]字符集合中包含哪些字元還真不知道。

問:不知道字符集合[!-~]裡有啥怎麼?
答:動手做實驗。以下是實驗程式碼,有興趣的童鞋可將程式碼儲存為.html格式檔案,並在瀏覽器中檢視執行結果(此程式碼已在32位Win7-IE9下除錯通過):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<body>
<script type="text/javascript">
<!--
document.write("西文可見字元列表<br />");
var str = "!-~"
var westCharList = document.body.appendChild(document.createElement("ol"));
var westCharItem;
for (var i=str.charCodeAt(0); i<=str.charCodeAt(2); i++)
{
    westCharItem = document.createElement("li");
    westCharItem.innerText = "Char Code: " + i + "; Char: " + String.fromCharCode(i);
    westCharList.appendChild(westCharItem);
}
//-->
</script>  
</body>
</html>

程式碼1:顯示 [!-~] 字符集合字元列表

【俺】 [!-~] 匹配的是從 Char Code: 33; Char: ! 至 Char Code: 126; Char: ~ 的全部字元,即英文鍵盤上的94個可見字元。即 [!-~] 是西文和數字字符集合。
【李】這樣用JS就能控制了。

問:雖然 [!-~] 的含義清楚了,但是 /([!-~]) ([^!-~])/ 到底能匹配什麼內容?
答:繼續實驗。程式碼如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<body>
<script type="text/javascript">
<!--
var reg = /([!-~]) ([^!-~])/g;
var str = "Start 在西班牙123“The rain in Spain ”雨水 falls 主要——mainly 集中在out 《in》the plain.集wuwu中在平原地區。End";
var matchResult = str.match(reg);
for (var i=0; i<matchResult.length; i++)
{
    document.write(matchResult[i]+"<br />");
}    
//-->
</script>  
</body>
</html>

程式碼2:顯示正規表示式 /([!-~]) ([^!-~])/g 匹配內容

輸出結果如下:

t 在
n ”
s 主
y 集
t 《

根據實驗結果可知,正規表示式 /([!-~]) ([^!-~])/g 用於檢查西文字元(在前)與非西文字元(在後)之間的空格,如果配合使用字串的 replace 方法可實現去除空格的目的。

慢著慢著,不是說要插入空格麼?
別急別急,既然能刪除空格了,那麼只需稍加改造就能插入空格。程式碼如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
   <title>插入空格實驗程式碼1</title>
</head>
<body>
<script type="text/javascript">
<!--
var reg = /([!-~])([^!-~])/g;
var str = "Start在西班牙123“The rain in Spain”雨水 falls主要——mainly集中在out《in》the plain.集wuwu中在平原地區。End";
var redSpace = "<span style='background-color: red;'> </span>";
var matchResult = str.match(reg);
for (var i=0; i<matchResult.length; i++)
{
    document.write(matchResult[i]+"<br />");
}
document.write(str.replace(reg, "$1"+redSpace+"$2")+"<br />");
//-->
</script>  
</body>
</html>

程式碼3:在西文尾部插入空格

在西文尾部插入空格輸出結果

圖2:程式碼3輸出結果

雖然插入了空格,但是問題也不少:

  1. 西文之間的空格被替換了。希望不要替換,原樣保留。
  2. 西文與後面的中文標點之間被插入了空格。希望不要插入空格,原樣保留。
  3. 西文與前面的中文之間尚未插入空格。希望插入空格,若前面是標點,則不要插入空格,原樣保留。

常言道,一口吃不成個胖子。問題要一個一個地解決。

  1. 西文之間的空格被替換了。希望不要替換,原樣保留。

這個不難,只需將空格加入集合,修改後的正規表示式如下(請自行實驗):

var reg = /([!-~ ])([^!-~ ])/g;

去除西文之間空格的輸出結果

圖3:基於程式碼3修正問題1後的輸出結果

問題2是中文標點判斷問題,而且問題3也涉及此問題,不過俺想先解決問題3“中西文與前面的中文之間尚未插入空格”的問題,因為它與問題1很相似,僅僅是位置變化了。程式碼如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<body>
<script type="text/javascript">
<!--
var reg = /([^!-~ ])([!-~ ]+)([^!-~ ])/g;
var str = "Start在西班牙123“The rain in Spain”雨水 falls主要——mainly集中在out《in》the plain.集wuwu中在平原地區。End";
var redSpace = "<span style='background-color: red;'> </span>";
var matchResult = str.match(reg);
for (var i=0; i<matchResult.length; i++)
{
    document.write(matchResult[i]+"<br />");
}
document.write(str.replace(reg, "$1"+redSpace+"$2"+redSpace+"$3")+"<br />");
//-->
</script>  
</body>
</html>

程式碼4:在西文頭、尾部同時插入空格

程式碼4輸出結果

圖4:程式碼4的輸出結果

仔細觀察不難發現,幾處本該插入空格的地方並沒有空格,顯然正規表示式仍需要調整,修正後正規表示式如下:

var reg = /([^!-~ ]*)([!-~ ]+)([^!-~ ]?)/g;

程式碼4修正後的輸出結果

圖5:程式碼4修正後的輸出結果

至此,尚未解決的問題是:當西文頭、尾部出現中文標點符號時不應插入空格,保留原樣即可。

通過仔細分析圖5的輸出結果不難看出,中文符號的問題在一次匹配中無法成功解決,因為有些情況下要依賴於前次匹配末尾字元來輔助判斷,如“中在out《”與“in》”就屬於此類情況。

儘管用正規表示式難以處理,但是還有一招,即 replace 方法,因為該方法的第二引數可以接受“返回替換文字的函式”,這就好辦了。程式碼如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<body>
<script type="text/javascript">
<!--
String.prototype.Trim= function(){
    return this.replace(/(^\s*)|(\s*$)/g, "");
};
String.prototype.GetLastChar= function(){
    return this.charAt(this.length-1);
};
String.prototype.IsNullOrEmpty= function(){
    return (this==null) || (this=="");
};
function IsChinesePunctuation(givenChar) {
    var regCnPunctuations = /[!¥……()——【】;:‘’“”,。、《》?]/;
    return regCnPunctuations.test(givenChar);
}

var reg = /([^!-~ ]*)([!-~ ]+)([^!-~ ]?)/g;
var str = "Start在西班牙123“The rain in Spain”雨水 falls主要——mainly集中在out《in》the plain.集wuwu中在平原地區。End";
var redSpace = "<span style='background-color: red;'> </span>";
var matchResult = str.match(reg);
for (var i=0; i<matchResult.length; i++)
{
    document.write(matchResult[i]+"<br />");
}
var previousTailIsCnPunc = false;
document.write(str.replace(reg,
    function($0,$1,$2,$3) {
            var output = $1;
            var redSpace = "<span style='background-color: red;'> </span>";
            // $1為西文前面的0-n箇中文字元,當$1的最後一箇中文字元為標點符號,或$1為空字串且前此匹配尾部為標點符號時,則不插入空格,直接拼接字串即可。
            if (IsChinesePunctuation($1.GetLastChar()) 
                || ($1.IsNullOrEmpty() && previousTailIsCnPunc))
                output += $2.Trim();
            else
                output += redSpace + $2.Trim();

            if (IsChinesePunctuation($3))
                output += $3;
            else
                output += redSpace + $3;

            previousTailIsCnPunc = IsChinesePunctuation($3);

            return output;
        }
    ));    
//-->
</script>
</body>
</html>

程式碼5:當西文與中文符號相鄰時不插入空格

程式碼5輸出結果

圖6:程式碼5的輸出結果

到此為止,在西文與中文之間插入空格的問題已基本解決了(歡迎大家捉蟲)。

不過俺更喜歡進一步控制樣式,且中文、西文分別用不同樣式來顯示,最終程式碼如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>《為在網頁中插入「空格」編寫的JS指令碼》</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="generator" content="editplus" />
    <meta name="author" content="http://weibo.com/yixianggao" />
    <meta name="keywords" content="JavaScript RegExp typography CSS style space" />
    <meta name="description" content="" />
    <style type="text/css">
span.chineseText {
    font-family: "微軟雅黑", "宋體";
    font-size: 1em;
    letter-spacing: .1em;
    color: #004040;
}
span.westText {
    font-family: "Segoe UI", Verdana, Helvetica, Calibri, Tahoma;
    font-size: 1em;
    color: #0080c0;
}
span.leftGap {
    margin-left: .2em;
}
span.rightGap {
    margin-right: .2em;
}
    </style>
</head>
<body>
<h3>《為在網頁中插入「空格」編寫的JS指令碼》</h3>
<script type="text/javascript">
<!--
String.prototype.Trim= function(){
    return this.replace(/(^\s*)|(\s*$)/g, "");
};
String.prototype.GetLastChar= function(){
    return this.charAt(this.length-1);
};
String.prototype.IsNullOrEmpty= function(){
    return (this==null) || (this=="");
};
function WriteLine(outputs) {
    for (var i=0; i<arguments.length; i++)
    {
        if (arguments[i] instanceof Array)
        {
            for (var j=0; j<arguments[i].length; j++)
                WriteLine(arguments[i][j]);
        }
        else
            document.write(arguments[i]);
    }
    document.write("<br />");
}
function IsChinesePunctuation(givenChar) {
    var regCnPunctuations = /[!¥……()——【】;:‘’“”,。、《》?]/;
    return regCnPunctuations.test(givenChar);
}
function SpliceWestTextClass(headChineseChar, tailChineseChar, prevTailIsCnPunc) {
    var westTextClass = "westText ";

    if ((!headChineseChar.IsNullOrEmpty() && !IsChinesePunctuation(headChineseChar))
        || (headChineseChar.IsNullOrEmpty() && !prevTailIsCnPunc))
        westTextClass += "leftGap ";

    if (!IsChinesePunctuation(tailChineseChar))
        westTextClass += "rightGap ";

    return westTextClass;
}
function BuildTextWithClassName(plainText, className) {
    if (!plainText.IsNullOrEmpty())
        return "<span class='" + className + "'>" + plainText.Trim() + "</span>";
    else
        return "";
}

var strTest = "Netscape在最初將其指令碼語言命名為LiveScript,後來Netscape在與Sun合作之後將其改名為JavaScript。JavaScript最初受Java啟發而開始設計的,目的之一就是“看上去像Java”,因此語法上有類似之處,一些名稱和命名規範也借自Java。但JavaScript的主要設計原則源自Self和Scheme。JavaScript與Java名稱上的近似,是當時網景為了營銷考慮與太陽微系統達成協議的結果。為了取得技術優勢,微軟推出了JScript來迎戰JavaScript的指令碼語言。為了互用性,Ecma國際(前身為歐洲計算機制造商協會)建立了ECMA-262標準(ECMAScript)。現在兩者都屬於ECMAScript的實現。儘管JavaScript作為給非程式人員的指令碼語言,而非作為給程式人員的程式語言來推廣和宣傳,但是JavaScript具有非常豐富的特性。";

// (0-n中文字元)(1-n西文字元)(0-1中文字元)
var regWestChars = /([^!-~]*)([!-~ ]*)([^!-~]?)/g;
var previousTailIsCnPunc = true;

WriteLine("<hr />");
WriteLine("<b>原文直接輸出</b>");
WriteLine(strTest);

/* For debug.
WriteLine("<hr />");
WriteLine(strTest.match(regWestChars));
//*/

WriteLine("<hr />");
WriteLine("<b>動態樣式輸出</b>");
WriteLine(strTest.replace(regWestChars,
    function($0,$1,$2,$3) {
            var westTextClass = SpliceWestTextClass($1.GetLastChar(), $3, previousTailIsCnPunc);
            previousTailIsCnPunc = IsChinesePunctuation($3);
            return BuildTextWithClassName($1, "chineseText") + BuildTextWithClassName($2, westTextClass) + BuildTextWithClassName($3, "chineseText");
        }
    ));

//-->
</script>
</body>
</html>

程式碼6:動態設定純文字中西文顯示樣式

動態設定純文字中西文顯示樣式輸出結果

圖7:程式碼6的輸出結果

【注】以上文字節選自維基百科JavaScript詞條。

謝謝大家能看到這裡,任何意見和建議請不吝賜教 :D

相關文章