Mathcenter Community


MathJaX ชุดคำสั่งสำหรับแสดงสมการคณิตศาสตร์บนเว็บ

บทความ/จัดพิมพ์ โดย: นาย เจษฏา กานต์ประชา


MathJaX กับภาษาไทย

ปัญหาการแสดงผลภาษาไทยเป็นปัญหาเฉพาะสำหรับคนไทย จึงขอแยกออกจากปัญหาทั่วไป ปัญหาการแสดงภาษาไทยที่ปนอยู่ในสมการคณิตศาสตร์ของ MathJaX นั้น มี 2 ปัญหาหลักดังนี้

  1. ตัวอักษรภาษาไทยในสมการคณิตศาสตร์ใช้ฟอนต์คนละตัวกับตัวอักษรภาษาไทยบนเว็บ

    ปัญหานี้เกิดจากการไม่ได้กำหนดฟอนต์สำหรับการแสดงผลอื่นๆที่ไม่ใช่สมการคณิตศาสตร์

    วิธีแก้ไข เราสามารถตั้งค่า MathJaX ให้เลือกใช้งานฟอนต์ตัวเดียวกับบนหน้าเว็บได้ จากพารามิเตอร์ undefinedFamily ตัวอย่างสำหรับ SVG เช่น

    MathJax.Hub.Config({
    	...
    	"SVG": {
    		...
    		undefinedFamily:"Tahoma",
    		...
    	},
    	...
    });
    								

  2. สระหรือวรรณยุกต์ ที่ลอยอยู่เหนือหรือใต้ตัวพยัญชนะ จะมองไม่เห็นทั้งหมด

    ปัญหานี้จะเกิดเมื่อเลือกวิธีแสดงผลเป็น SVG (จริงแล้วๆควรจะเกิดกับ HTML-CSS ด้วยเพียงแต่บราวเซอร์ส่วนใหญ่จะแสดงผลได้ไม่มีปัญหา) ตัวอย่างเช่น เมื่อเราเขียนคำสั่ง LaTeX เป็น \frac{พื้นที่สามเหลี่ยม}{พื้นที่สี่เหลี่ยม} จะแสดงผลดังรูปแรก แทนที่จะแสดงผลดังรูปถัดไป


    สาเหตุที่ทำให้ MathJaX แสดงผลแบบนี้ นั่นเป็นเพราะ MathJaX แยกการแสดงผลตัวอักษรแต่ละตัวออกจากกันอย่างเด็ดขาด แทนที่จะมองเป็นข้อความยาวๆว่า พื้นที่สี่เหลี่ยม แต่จะมองเป็น พ ื ้ นท ี ่ส ี ่เหล ี ่ยม

    สระหรือวรรณยุกต์ที่ลอยหรืออยู่ใต้พยัญชนะเหล่านี้ บนบราวเซอร์บางตัว เช่น Chrome จะไม่สามารถแสดงผล สระหรือวรรณยุกต์ตัวเดียวโดดๆได้ ผลลัพธ์ก็คือมันจะไม่ถูกแสดงผล

    วิธีแก้ไข ทำได้ 2 วิธี ดังนี้

    1. กำหนดพารามิเตอร์ 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", ...],
      	...
      });
      											

    2. Hack MathJaX

      เป็นวิธีที่เว็บ Mathcenter.net ใช้ ก่อนจะคิดวิธีใช้พารามิเตอร์ mtextFontInherit + Extension ThaiFix เป็นผลสำเร็จ วิธีนี้มีข้อเสียคือ ต้องหมั่น Hack MathJaX ทุกครั้งที่จะอัพเกรด MathJaX ไปใช้เวอร์ชันใหม่ มีจุดที่ต้องแก้ไข 2 จุดใหญ่คือ

      1. 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)}
        	}
        },
        ...
        														

      2. 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