บทความ/จัดพิมพ์ โดย: นาย เจษฏา กานต์ประชา
ปัญหาการแสดงผลภาษาไทยเป็นปัญหาเฉพาะสำหรับคนไทย จึงขอแยกออกจากปัญหาทั่วไป ปัญหาการแสดงภาษาไทยที่ปนอยู่ในสมการคณิตศาสตร์ของ MathJaX นั้น มี 2 ปัญหาหลักดังนี้
ตัวอักษรภาษาไทยในสมการคณิตศาสตร์ใช้ฟอนต์คนละตัวกับตัวอักษรภาษาไทยบนเว็บ
ปัญหานี้เกิดจากการไม่ได้กำหนดฟอนต์สำหรับการแสดงผลอื่นๆที่ไม่ใช่สมการคณิตศาสตร์
วิธีแก้ไข เราสามารถตั้งค่า MathJaX ให้เลือกใช้งานฟอนต์ตัวเดียวกับบนหน้าเว็บได้ จากพารามิเตอร์ undefinedFamily ตัวอย่างสำหรับ SVG เช่น
MathJax.Hub.Config({ ... "SVG": { ... undefinedFamily:"Tahoma", ... }, ... });
สระหรือวรรณยุกต์ ที่ลอยอยู่เหนือหรือใต้ตัวพยัญชนะ จะมองไม่เห็นทั้งหมด
ปัญหานี้จะเกิดเมื่อเลือกวิธีแสดงผลเป็น SVG (จริงแล้วๆควรจะเกิดกับ HTML-CSS ด้วยเพียงแต่บราวเซอร์ส่วนใหญ่จะแสดงผลได้ไม่มีปัญหา) ตัวอย่างเช่น เมื่อเราเขียนคำสั่ง LaTeX เป็น \frac{พื้นที่สามเหลี่ยม}{พื้นที่สี่เหลี่ยม} จะแสดงผลดังรูปแรก แทนที่จะแสดงผลดังรูปถัดไป
สาเหตุที่ทำให้ MathJaX แสดงผลแบบนี้ นั่นเป็นเพราะ MathJaX แยกการแสดงผลตัวอักษรแต่ละตัวออกจากกันอย่างเด็ดขาด แทนที่จะมองเป็นข้อความยาวๆว่า พื้นที่สี่เหลี่ยม แต่จะมองเป็น พ ื ้ นท ี ่ส ี ่เหล ี ่ยม
สระหรือวรรณยุกต์ที่ลอยหรืออยู่ใต้พยัญชนะเหล่านี้ บนบราวเซอร์บางตัว เช่น Chrome จะไม่สามารถแสดงผล สระหรือวรรณยุกต์ตัวเดียวโดดๆได้ ผลลัพธ์ก็คือมันจะไม่ถูกแสดงผล
วิธีแก้ไข ทำได้ 2 วิธี ดังนี้
กำหนดพารามิเตอร์ mtextFontInherit + Extension ThaiFix
วิธีนี้เวลาใช้ภาษาไทย เราไม่สามารถพิมพ์ภาษาไทยลงไปตรงๆ จะต้องใช้ผ่านคำสั่ง \text{} ยกตัวอย่างเช่น "\frac{\text{พื้นที่สามเหลี่ยม}}{\text{พื้นที่สี่เหลี่ยม}}"
วิธีแก้ไข กำหนดพารามิเตอร์ mtextFontInherit เป็น true เช่น
MathJax.Hub.Config({ ... "SVG": { ... mtextFontInherit: true, ... }, ... });
จะพบว่าวิธีนี้ใช้ได้ดีกับเว็บที่เราเป็นคนเขียนเนื้อหาเอง (เพราะเราสามารถใช้ภาษาไทยผ่านคำสั่ง \text{}) แต่หากนำมาใช้กับเว็บบอร์ดที่สมาชิกเขียนแสดงความคิดเห็นหลากหลายสไตล์ ไม่สนใจจะใช้ผ่านคำสั่งดังกล่าว ก็จะพบปัญหานี้ดังเดิม เราสามารถแก้ปัญหานี้โดยเขียน Extension ThaiFix เพิ่มเติม ช่วยค้นหาตำแหน่งของตัวอักษรภาษาไทยทั้งหมดที่ไม่ได้ใช้ผ่านคำสั่ง \text{} แล้วเพิ่มคำสั่ง \text{} เข้าไป
extensions/ThaiFix.js
/************************************************************* * * MathJax/extensions/ThaiFix.js * * Implements Thai characters to Jax preprocessor that locates Thai characters * within the text of a document and replaces it with \text{ตัวหนังสือไทย} tags * for processing by MathJax. * * Created by Jetsada Karnpracha on 24th May 2014 */ MathJax.Extension.ThaiFix = { version: "1.0", config: { preview: "TeX"}, PreProcess: function (element) { if (!this.configured) { this.config = MathJax.Hub.CombineConfig("ThaiFix",this.config); if (this.config.Augment) {MathJax.Hub.Insert(this,this.config.Augment)} if (typeof(this.config.previewTeX) !== "undefined" && !this.config.previewTeX) {this.config.preview = "none"} // backward compatibility for previewTeX parameter this.previewClass = MathJax.Hub.config.preRemoveClass; this.configured = true; } if (typeof(element) === "string") {element = document.getElementById(element)} if (!element) {element = document.body} var script = element.getElementsByTagName("script"), i; var regexThaiCharacter = /(\\text\{[^}]*)?([\u0E00-\u0E7F]+)/g; for (i = script.length-1; i >= 0; i--) { if (String(script[i].type).match(/math\/tex/)) { script[i].innerHTML = script[i].innerHTML.replace(regexThaiCharacter, function($0, $1, $2) { return $1 ? $0: "\\text{" + $2 + "}"}); } } }, }; MathJax.Hub.Register.PreProcessor(["PreProcess",MathJax.Extension.ThaiFix]); MathJax.Ajax.loadComplete("[MathJax]/extensions/ThaiFix.js");
ตัวอย่างการเรียกใช้ Extension ThaiFix
MathJax.Hub.Config({ ... extensions: ["tex2jax.js", "ThaiFix.js", ...], ... });
Hack MathJaX
เป็นวิธีที่เว็บ Mathcenter.net ใช้ ก่อนจะคิดวิธีใช้พารามิเตอร์ mtextFontInherit + Extension ThaiFix เป็นผลสำเร็จ วิธีนี้มีข้อเสียคือ ต้องหมั่น Hack MathJaX ทุกครั้งที่จะอัพเกรด MathJaX ไปใช้เวอร์ชันใหม่ มีจุดที่ต้องแก้ไข 2 จุดใหญ่คือ
InputJax
เป็นโค้ดแปลงข้อมูลที่ผู้ใช้ป้อนเข้ามาให้อยู่ในรูปแบบ mml (เป็นโครงสร้างเดียวกับ MathML) ข้อมูลที่ผู้ใช้ป้อนเข้ามาทุกรูปแบบจะถูกแปลงเป็นรูปแบบ mml ทั้งหมด เพื่อสะดวกในการนำไปแปลงเป็นรูปแบบอื่นๆสำหรับแสดงผลบนหน้าเว็บต่อไป เราจะต้องแก้ไม่ให้ MathJaX แยกข้อความภาษาไทยที่อยู่ติดกัน ออกเป็นตัวอักษรแต่ละตัว แต่ให้เก็บทั้งหมดลงใน 1 mml element
ตัวอย่างการแก้ไขใน MathJaX 2.3 (บรรทัดที่ทำแถบสีไว้ เป็นบรรทัดที่มีการแก้ไข หรือเป็นบรรทัดใหม่เพิ่มเข้ามา)
jax/input/TeX/jax.js
... Parse: function () { var c, n; var cThai, nThai, szThai; while (this.i < this.string.length) { c = this.string.charAt(this.i++); n = c.charCodeAt(0); if (n >= 0x0E00 && n <= 0x0E7F) { cThai = c; nThai = n; szThai = cThai; while (this.i < this.string.length) { cThai = this.string.charAt(this.i++); nThai = cThai.charCodeAt(0); if (nThai >= 0x0E00 && nThai <= 0x0E7F) { szThai += cThai; } else { this.i--; break; } } } if (n >= 0xD800 && n < 0xDC00) {c += this.string.charAt(this.i++)} if (TEXDEF.special[c]) {this[TEXDEF.special[c]](c)} else if (TEXDEF.letter.test(c)) {this.Variable(c)} else if (TEXDEF.digit.test(c)) {this.Number(c)} else if (n >= 0x0E00 && n < 0x0E7F) { this.Other(szThai); } else {this.Other(c)} } }, ...
OutputJax
เป็นโค้ดแปลงข้อมูลในรูปแบบ mml เป็นรูปแบบสำหรับแสดงผลบนหน้าเว็บ เราจะต้องแก้ไขให้ MathJaX นำข้อความภาษาไทยทั้งหมดที่อยู่ใน 1 mml element ขึ้นแสดงผลทั้งหมดใน 1 output element (ขึ้นกับเทคนิคการแสดงผลที่ใช้)
ตัวอย่างการแก้ไขใน MathJaX 2.3 (บรรทัดที่ทำแถบสีไว้ เป็นบรรทัดที่มีการแก้ไข หรือเป็นบรรทัดใหม่เพิ่มเข้ามา)
jax/output/SVG/jax.js
... HandleVariant: function (variant,scale,text) { var svg = BBOX.G(); var n, N, c, font, VARIANT, i, m, id, M, RANGES; if (!variant) {variant = this.FONTDATA.VARIANT[MML.VARIANT.NORMAL]} if (variant.forceFamily) { text = BBOX.TEXT(scale,text,variant.font); if (variant.h != null) {text.h = variant.h}; if (variant.d != null) {text.d = variant.d} svg.Add(text); text = ""; } VARIANT = variant; var cThai, nThai, szThai; for (i = 0, m = text.length; i < m; i++) { variant = VARIANT; n = text.charCodeAt(i); c = text.charAt(i); if (n >= 0xD800 && n < 0xDBFF) { i++; n = (((n-0xD800)<<10)+(text.charCodeAt(i)-0xDC00))+0x10000; if (this.FONTDATA.RemapPlane1) { var nv = this.FONTDATA.RemapPlane1(n,variant); n = nv.n; variant = nv.variant; } } else if (n >= 0x0E00 && n <= 0x0E7F) { // Group Thai Characters Together nThai = text.charCodeAt(i); cThai = text.charAt(i); szThai = cThai; i++; while (i < m) { nThai = text.charCodeAt(i); cThai = text.charAt(i); if (nThai >= 0x0E00 && nThai <= 0x0E7F) { szThai += cThai; i++; } else break; } c = szThai; i--; } else { RANGES = this.FONTDATA.RANGES; for (id = 0, M = RANGES.length; id < M; id++) { if (RANGES[id].name === "alpha" && variant.noLowerCase) continue; N = variant["offset"+RANGES[id].offset]; if (N && n >= RANGES[id].low && n <= RANGES[id].high) { if (RANGES[id].remap && RANGES[id].remap[n]) { n = N + RANGES[id].remap[n]; } else { n = n - RANGES[id].low + N; if (RANGES[id].add) {n += RANGES[id].add} } if (variant["variant"+RANGES[id].offset]) {variant = this.FONTDATA.VARIANT[variant["variant"+RANGES[id].offset]]} break; } } } if (variant.remap && variant.remap[n]) { if (variant.remap[n] instanceof Array) { var remap = variant.remap[n]; n = remap[0]; variant = this.FONTDATA.VARIANT[remap[1]]; } else if (typeof(variant.remap[n]) === "string") { text = variant.remap[n]+text.substr(i+1); i = 0; m = text.length; n = text.charCodeAt(0); } else { n = variant.remap[n]; if (variant.remap.variant) {variant = this.FONTDATA.VARIANT[variant.remap.variant]} } } if (this.FONTDATA.REMAP[n] && !variant.noRemap) { n = this.FONTDATA.REMAP[n]; if (n instanceof Array) {variant = this.FONTDATA.VARIANT[n[1]]; n = n[0]} if (typeof(n) === "string") { text = n+text.substr(i+1); i = 0; m = text.length; n = n.charCodeAt(0); } } // Lookup Only When Not Thai Characters if (n < 0x0E00 || n > 0x0E7F) { font = this.lookupChar(variant,n); c = font[n]; } if ((n < 0x0E00 || n > 0x0E7F) && c) { if (c[5] && c[5].space) {svg.w += c[2]} else { c = [scale,font.id+"-"+n.toString(16).toUpperCase()].concat(c); svg.Add(BBOX.GLYPH.apply(BBOX,c),svg.w,0); } } else if (this.FONTDATA.DELIMITERS[n]) { c = this.createDelimiter(n,0,1,font); svg.Add(c,svg.w,(this.FONTDATA.DELIMITERS[n].dir === "V" ? c.d: 0)); } else { if (n <= 0xFFFF) { if (n < 0x0E00 || n > 0x0E7F) { c = String.fromCharCode(n); } // Convert Only When Not Thai Characters } else { N = n - 0x10000; c = String.fromCharCode((N>>10)+0xD800) + String.fromCharCode((N&0x3FF)+0xDC00); } var box = BBOX.TEXT(scale,c,{ "font-family":variant.defaultFamily||SVG.config.undefinedFamily, "font-style":(variant.italic?"italic":""), "font-weight":(variant.bold?"bold":"") }) if (variant.h != null) {box.h = variant.h}; if (variant.d != null) {box.d = variant.d} c = BBOX.G(); c.Add(box); svg.Add(c,svg.w,0); HUB.signal.Post(["SVG Jax - unknown char",n,variant]); } } if (text.length == 1 && font && font.skew && font.skew[n]) {svg.skew = font.skew[n]*1000} if (svg.element.childNodes.length === 1) { svg.element = svg.element.firstChild; svg.removeable = false; svg.scale = scale; } return svg; }, ...
jax/output/HTML-CSS/jax.js
... handleVariant: function (span,variant,text) { var newtext = "", n, c, font, VARIANT, SPAN = span, force = !!span.style.fontFamily; if (text.length === 0) return; if (!span.bbox) { span.bbox = { w: 0, h: -this.BIGDIMEN, d: -this.BIGDIMEN, rw: -this.BIGDIMEN, lw: this.BIGDIMEN }; } if (!variant) {variant = this.FONTDATA.VARIANT[MML.VARIANT.NORMAL]} VARIANT = variant; for (var i = 0, m = text.length; i < m; i++) { variant = VARIANT; n = text.charCodeAt(i); c = text.charAt(i); if (n >= 0xD800 && n < 0xDBFF) { i++; n = (((n-0xD800)<<10)+(text.charCodeAt(i)-0xDC00))+0x10000; if (this.FONTDATA.RemapPlane1) { var nv = this.FONTDATA.RemapPlane1(n,variant); n = nv.n; variant = nv.variant; } } else { var id, M, RANGES = this.FONTDATA.RANGES; for (id = 0, M = RANGES.length; id < M; id++) { if (RANGES[id].name === "alpha" && variant.noLowerCase) continue; var N = variant["offset"+RANGES[id].offset]; if (N && n >= RANGES[id].low && n <= RANGES[id].high) { if (RANGES[id].remap && RANGES[id].remap[n]) { n = N + RANGES[id].remap[n]; } else { n = n - RANGES[id].low + N; if (RANGES[id].add) {n += RANGES[id].add} } if (variant["variant"+RANGES[id].offset]) {variant = this.FONTDATA.VARIANT[variant["variant"+RANGES[id].offset]]} break; } } } if (variant.remap && variant.remap[n]) { if (variant.remap[n] instanceof Array) { var remap = variant.remap[n]; n = remap[0]; variant = this.FONTDATA.VARIANT[remap[1]]; } else if (typeof(variant.remap[n]) === "string") { text = variant.remap[n]+text.substr(i+1); i = 0; m = text.length; n = text.charCodeAt(0); } else { n = variant.remap[n]; if (variant.remap.variant) {variant = this.FONTDATA.VARIANT[variant.remap.variant]} } } if (this.FONTDATA.REMAP[n] && !variant.noRemap) { n = this.FONTDATA.REMAP[n]; if (n instanceof Array) {variant = this.FONTDATA.VARIANT[n[1]]; n = n[0]} if (typeof(n) === "string") { text = n+text.substr(i+1); i = 0; m = text.length; n = n.charCodeAt(0); } } font = this.lookupChar(variant,n); c = font[n]; if (force || (!this.checkFont(font,SPAN.style) && !c[5].img)) { if ((n < 0x0E00 || n > 0x0E7F) && newtext.length) {this.addText(SPAN,newtext); newtext = ""}; var addSpan = !!SPAN.style.fontFamily || !!span.style.fontStyle || !!span.style.fontWeight || !font.directory || force; force = false; if (SPAN !== span) {addSpan = !this.checkFont(font,span.style); SPAN = span} if (addSpan) {SPAN = this.addElement(span,"span",{isMathJax:true, subSpan:true})} this.handleFont(SPAN,font,SPAN !== span); } newtext = this.handleChar(SPAN,font,c,n,newtext); if (!(c[5]||{}).space) { if (c[0]/1000 > span.bbox.h) {span.bbox.h = c[0]/1000} if (c[1]/1000 > span.bbox.d) {span.bbox.d = c[1]/1000} } if (span.bbox.w + c[3]/1000 < span.bbox.lw) {span.bbox.lw = span.bbox.w + c[3]/1000} if (span.bbox.w + c[4]/1000 > span.bbox.rw) {span.bbox.rw = span.bbox.w + c[4]/1000} span.bbox.w += c[2]/1000; } if (newtext.length) {this.addText(SPAN,newtext)} if (span.scale && span.scale !== 1) { span.bbox.h *= span.scale; span.bbox.d *= span.scale; span.bbox.w *= span.scale; span.bbox.lw *= span.scale; span.bbox.rw *= span.scale; } if (text.length == 1 && font.skew && font.skew[n]) {span.bbox.skew = font.skew[n]} }, ...
Javascript Compressor
คำแนะนำ หากเราสร้างไฟล์ตั้งค่า หรือ Extension ใช้งานเอง ตอนนำไปใช้งานจริง ควรจะบีบอัดไฟล์ Javascript ให้มีขนาดเล็กลง เพื่อความรวดเร็วตอนดาวน์โหลดและแสดงผล เครื่องมือที่แนะนำให้ใช้ คือ yuicompressor 2.4.7 (ต้องมี Java JRE เวอร์ชัน 1.4 ขึ้นไปติดตั้งบนเครื่อง)
วิธีเรียกใช้
java -jar yuicompressor-2.4.7.jar <UnCompressJavascript.js> -o <CompressJavascript.js>ตัวอย่างการเรียกใช้งานเช่น
java -jar yuicompressor-2.4.7.jar MyExtension.js -o MyExtension.min.js