Mathcenter Community


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

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


FindTeX Option

นอกจากการทำให้ใช้ภาษาไทยได้ผ่าน option renderActions เราสามารถแก้ไขผ่าน option FindTeX ได้ด้วย ซึ่งจะทำให้เราสามารถจัดการกับ LaTeX ที่แสดงผลได้มากขึ้น เพราะปกติแล้ว MathJaX จะไม่ได้ค้นหา LaTeX ที่ต้องการแสดงผลจาก inline/display delimiter เท่านั้น แต่จะดูเนื้อหาข้างในด้วยว่า มีวงเล็บ {} ครบคู่หรือไม่ หากมีไม่ครบคู่ก็จะข้าม ไม่ยอมประมวลผล และไม่แจ้งเตือนอะไรทั้งสิ้นว่าเกิดจากปัญหาอะไร เราสามารถแก้ปัญหาตรงนี้ได้ด้วยการตั้งค่า FindTeX Option

สิ่งที่ต้องใส่ใน FindTeX Option จะเป็น Object instance ที่มี method findMath โดย MathJaX จะเรียกใช้ method นี้ผ่าน method findMath ของ Class input/TeX

TeX.prototype.findMath = function (strings) {
	return this.findTeX.findMath(strings);
};
						

สิ่งที่ส่งเข้ามาใน method คือ strings เป็นอาเรย์ของข้อความที่ต้องการให้ตรวจสอบว่า มีข้อความไหนที่มี LaTeX หรือไม่ ยกตัวอย่างเช่น

[
	"Hello",
	"Test \\[\\frac{abc}{def}\\] English",
	"World",
	"Go \\(\\sqrt{b^2 - 4ac}\\) Message \\(\\pm \\sqrt{2}\\) Hello"
]
						

ผลลัพธ์ที่ส่งกลับมาจาก method เป็นอาเรย์ของ Object แจ้งรายละเอียดของ LaTeX ที่เจอในข้อความทั้งหมด ยกตัวอย่างเช่น

[
	{
		"open": "\\[",
		"math": "\\frac{abc}{def}",
		"close": "\\]",
		"n": 1,
		"start": {
			"n": 5
		},
		"end": {
			"n": 24
		},
		"display": true
	},
	{
		"open": "\\(",
		"math": "\\sqrt{b^2 - 4ac}",
		"close": "\\)",
		"n": 3,
		"start": {
			"n": 3
		},
		"end": {
			"n": 23
		},
		"display": false
	},
	{
		"open": "\\(",
		"math": "\\pm \\sqrt{2}",
		"close": "\\)",
		"n": 3,
		"start": {
			"n": 32
		},
		"end": {
			"n": 48
		},
		"display": false
	}
]
						

แต่ละฟิลด์มีความหมายดังนี้

  • open Open delimiter ของ LaTeX
  • math โค้ด LaTeX (ไม่รวม Delimiter)
  • close Close delimiter ของ LaTeX
  • n ตำแหน่ง Index ของอาเรย์ข้อวาม ที่ตรวจพบ LaTeX (Index เริ่มต้นจาก 0)
  • start.n Offset ตำแหน่งเริ่มต้นในข้อความที่เจอ LaTeX (รวม Delimiter ด้วย) (Offset เริ่มต้นจาก 0)
  • end.n Offset ตำแหน่งถัดจาก LaTeX ที่เจอในข้อความ (รวม Delimiter ด้วย) (Offset เริ่มต้นจาก 0)
  • display ต้องการให้แสดงผล LaTeX แบบ Display หรือไม่

Template เริ่มต้นสำหรับใช้ตั้งค่า FindTeX Option

const MyFindTeX = (function () {
	function MyFindTeX(options) {
		this.options = options;
	}

	MyFindTeX.prototype.findMath = function (strings) {
		// Todo: Define your findMath here
	};

	return MyFindTeX;
}());

const findTeXOptions = {
	// Todo: Define your option here
};
const myFindTeX = new MyFindTeX(findTeXOptions);

window.MathJax = {
	tex: {
		FindTeX: myFindTeX
	}
};
						

ตัวอย่างการแก้ไข Object instance ให้รองรับภาษาไทย และไม่ดูเนื้อหาข้างในว่า มีวงเล็บ {} ครบคู่หรือไม่

let MyFindTeX = (function () {
	function MyFindTeX(options) {
		this.options = options;
		this.init();
	}

	MyFindTeX.prototype.init = function () {
		const opens = [];
		this.closeInfo = {};

		for (const delims of this.options['inlineMath']) {
			this.addPattern(opens, delims, false)
		}
		for (const delims of this.options['displayMath']) {
			this.addPattern(opens, delims, true)
		}
		this.openPattern = new RegExp(opens.sort(this.sortLength).join('|'), 'g');
		this.regexThaiCharacter = /(\\text(?:rm)?\{[^}]*)?([\u0E00-\u0E7F]+)/g;
	}

	MyFindTeX.prototype.sortLength = function (a, b) {
		return a.length !== b.length ? b.length - a.length : a === b ? 0 : a < b ? -1 : 1;
	};

	MyFindTeX.prototype.addPattern = function (opens, delims, display) {
		const [open, close] = delims;
		opens.push(this.quotePattern(open));

		const closePattern = new RegExp(this.quotePattern(close), 'g');
		this.closeInfo[open] = [close, display, closePattern];
	};

	MyFindTeX.prototype.quotePattern = function (text) {
		return text.replace(/([\^$(){}+*?\-|\[\]\:\\])/g, '\\$1');
	};

	MyFindTeX.prototype.findMath = function (strings) {
		const maths = [];
		for (const [n, text] of strings.entries()) {
			let matchOpen, matchClose;
			this.openPattern.lastIndex = 0;

			while ((matchOpen = this.openPattern.exec(text))) {
				const [close, display, closePattern] = this.closeInfo[matchOpen[0]];
				const startMathIdx = closePattern.lastIndex = matchOpen.index + matchOpen[0].length;

				matchClose = closePattern.exec(text);
				if (matchClose[0] === close) {
					const endIdx = matchClose.index + matchClose[0].length;
					const math = text.substr(startMathIdx, matchClose.index - startMathIdx);
					const thaiMath = math.replace(this.regexThaiCharacter, function (match, has_textcmd_before, thai_content) {
						return has_textcmd_before ? match : "\\text{" + thai_content + "}";
					});
					maths.push({
						open: matchOpen[0],
						math: thaiMath,
						close: close,
						n: n,
						start: { n: matchOpen.index },
						end: { n: endIdx },
						display: display
					});
					this.openPattern.lastIndex = endIdx;
				}
			}
		}
		return maths;
	};

	return MyFindTeX;
}());

let findTeXOptions = {
	inlineMath: [
		['\\(', '\\)'],
		['$', '$']
	],
	displayMath: [
		['\\[', '\\]'],
		['$$', '$$']
	]
};
let myFindTeX = new MyFindTeX(findTeXOptions);

window.MathJax = {
	tex: {
		FindTeX: myFindTeX,
		macros: {
			bmatrix: ['\\begin{bmatrix}#1\\end{bmatrix}', 1],
			Bmatrix: ['\\begin{Bmatrix}#1\\end{Bmatrix}', 1],
			vmatrix: ['\\begin{vmatrix}#1\\end{vmatrix}', 1],
			Vmatrix: ['\\begin{Vmatrix}#1\\end{Vmatrix}', 1],
			cases: ['\\begin{cases}#1\\end{cases}', 1],
			euler: '\\atopwithdelims<>',
			sech: '\\text{sech}\\,'
		},
	},
	svg: {
		mtextInheritFont: true,
		scale: 1,
		fontCache: 'global'
	}
};
						

หากเราการต้องการแก้ไข Object instance จากตัวเริ่มต้นที่ MathJaX ให้มา ก็จำเป็นต้องคัดลอกสิ่งที่ MathJaX เตรียมมาให้ แล้วแก้ไขในส่วนที่ต้องการ ตัวอย่างโค้ดการใช้งาน FindTeX Option ที่แก้ไขให้รองรับภาษาไทย และไม่ดูเนื้อหาข้างในว่า มีวงเล็บ {} ครบคู่หรือไม่

const MyFindTeX = (function () {
	function MyFindTeX(options) {
		this.options = options;
		this.getPattern();
		this.regexThaiCharacter = /(\\text(?:rm)?\{[^}]*)?([\u0E00-\u0E7F]+)/g;
	}

	MyFindTeX.prototype.getPattern = function () {
		let options = this.options;
		let starts = [], parts = [], subparts = [];
		this.end = {};
		this.env = this.sub = 0;
		let i = 1;
		options['inlineMath'].forEach((delims) => this.addPattern(starts, delims, false));
		options['displayMath'].forEach((delims) => this.addPattern(starts, delims, true));
		if (starts.length) {
			parts.push(starts.sort(this.sortLength).join('|'));
		}
		if (options['processEnvironments']) {
			parts.push('\\\\begin\\s*\\{([^}]*)\\}');
			this.env = i;
			i++;
		}
		if (options['processEscapes']) {
			subparts.push('\\\\([\\\\$])');
		}
		if (options['processRefs']) {
			subparts.push('(\\\\(?:eq)?ref\\s*\\{[^}]*\\})');
		}
		if (subparts.length) {
			parts.push('(' + subparts.join('|') + ')');
			this.sub = i;
		}
		this.start = new RegExp(parts.join('|'), 'g');
		this.hasPatterns = (parts.length > 0);
	};

	MyFindTeX.prototype.sortLength = function (a, b) {
		return a.length !== b.length ? b.length - a.length : a === b ? 0 : a < b ? -1 : 1;
	};

	MyFindTeX.prototype.addPattern = function (starts, delims, display) {
		let [open, close] = delims;
		starts.push(this.quotePattern(open));
		this.end[open] = [close, display, this.endPattern(close)];
	};

	MyFindTeX.prototype.quotePattern = function (text) {
		return text.replace(/([\^$(){}+*?\-|\[\]\:\\])/g, '\\$1');
	};

	MyFindTeX.prototype.endPattern = function (end, endp) {
		return new RegExp((endp || this.quotePattern(end)) + '|\\\\(?:[a-zA-Z]|.)|[{}]', 'g');
	};

	MyFindTeX.prototype.findEnd = function (text, n, start, end) {
		let [close, display, pattern] = end;
		let i = pattern.lastIndex = start.index + start[0].length;
		let match;
		while ((match = pattern.exec(text))) {
			if ((match[1] || match[0]) === close) {
				return this.protoItem(start[0], text.substr(i, match.index - i), match[0],
					n, start.index, match.index + match[0].length, display);
			}
		}
		return null;
	};

	MyFindTeX.prototype.findMath = function (strings) {
		let math = [];
		for (let i = 0, m = strings.length; i < m; i++) {
			this.findMathInString(math, i, strings[i]);
		}
		return math;
	};

	MyFindTeX.prototype.protoItem = function (open, math, close, n, start, end, display = null) {
		let thaiMath = math.replace(this.regexThaiCharacter, function (match, has_textcmd_before, thai_content) {
			return has_textcmd_before ? match : "\\text{" + thai_content + "}";
		});
		return { open: open, math: thaiMath, close: close, n: n, start: { n: start }, end: { n: end }, display: display };
	};

	MyFindTeX.prototype.findMathInString = function (math, n, text) {
		let start, match;
		this.start.lastIndex = 0;
		while ((start = this.start.exec(text))) {
			if (start[this.env] !== undefined && this.env) {
				let end = '\\\\end\\s*(\\{' + this.quotePattern(start[this.env]) + '\\})';
				match = this.findEnd(text, n, start, ['{' + start[this.env] + '}', true, this.endPattern(null, end)]);
				if (match) {
					match.math = match.open + match.math + match.close;
					match.open = match.close = '';
				}
			} else if (start[this.sub] !== undefined && this.sub) {
				let math = start[this.sub];
				let end = start.index + start[this.sub].length;
				if (math.length === 2) {
					match = this.protoItem('', math.substr(1), '', n, start.index, end);
				} else {
					match = this.protoItem('', math, '', n, start.index, end, false);
				}
			} else {
				match = this.findEnd(text, n, start, this.end[start[0]]);
			}
			if (match) {
				math.push(match);
				this.start.lastIndex = match.end.n;
			}
		}
	};

	return MyFindTeX;
}());

const findTeXOptions = {
	inlineMath: [
		['\\(', '\\)'],
		['$', '$'],
	],
	displayMath: [
		['\\[', '\\]'],
		['$$', '$$']
	],
	processEscapes: true,
	processEnvironments: false,
	processRefs: false,
};
const myFindTeX = new MyFindTeX(findTeXOptions);

window.MathJax = {
	tex: {
		FindTeX: myFindTeX,
		macros: {
			bmatrix: ['\\begin{bmatrix}#1\\end{bmatrix}', 1],
			Bmatrix: ['\\begin{Bmatrix}#1\\end{Bmatrix}', 1],
			vmatrix: ['\\begin{vmatrix}#1\\end{vmatrix}', 1],
			Vmatrix: ['\\begin{Vmatrix}#1\\end{Vmatrix}', 1],
			cases: ['\\begin{cases}#1\\end{cases}', 1],
			euler: '\\atopwithdelims<>',
			sech: '\\text{sech}\\,'
		}
	},
	svg: {
		mtextInheritFont: true,
		scale: 1,
		fontCache: 'global'
	}
};
						

  • การแก้ไขให้รองรับภาษาไทย อยู่ใน method protoItem ระหว่างที่เตรียมข้อมูล Object รายละเอียดของ LaTeX ที่เจอในข้อความ ส่งกลับไป
  • ไม่ดูเนื้อหาข้างในว่า มีวงเล็บ {} ครบคู่หรือไม่ อยู่ใน method findEnd

จะสังเกตว่า Object instance ตัวเริ่มต้นของ MathJaX ใช้ค่า Option ต่อไปนี้เท่านั้น ค่า Option อื่นของ TeX นอกเหนือจากนี้ให้กำหนดนอก FindTeX

  • inlineMath
  • displayMath
  • processEnvironments
  • processEscapes
  • processRefs