Merge branch 'pryor'
[lambda.git] / jsMath / plugins / tex2math.js
1 /*
2  *  tex2math.js
3  *  
4  *  Part of the jsMath package for mathematics on the web.
5  *
6  *  This file is a plugin that searches text within a web page
7  *  for \(...\), \[...\], $...$ and $$...$$ and converts them to
8  *  the appropriate <SPAN CLASS="math">...</SPAN> or
9  *  <DIV CLASS="math">...</DIV> tags.
10  *
11  *  ---------------------------------------------------------------------
12  *
13  *  Copyright 2004-2007 by Davide P. Cervone
14  * 
15  *  Licensed under the Apache License, Version 2.0 (the "License");
16  *  you may not use this file except in compliance with the License.
17  *  You may obtain a copy of the License at
18  * 
19  *      http://www.apache.org/licenses/LICENSE-2.0
20  * 
21  *  Unless required by applicable law or agreed to in writing, software
22  *  distributed under the License is distributed on an "AS IS" BASIS,
23  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24  *  See the License for the specific language governing permissions and
25  *  limitations under the License.
26  */
27
28 if (!jsMath.tex2math) {jsMath.tex2math = {}}  // make sure jsMath.tex2math is defined
29 if (!jsMath.tex2math.loaded) {                // only load it once
30
31 if (!jsMath.Controls) {jsMath.Controls = {}}
32 if (!jsMath.Controls.cookie) {jsMath.Controls.cookie = {}}
33
34 jsMath.Add(jsMath.tex2math,{
35   
36   loaded: 1,
37   window: window,
38
39   /*
40    *  Call the main conversion routine with appropriate flags
41    */
42
43   ConvertTeX: function (element) {
44     this.Convert(element,{
45       processSingleDollars: 1, processDoubleDollars: 1,
46       processSlashParens: 1, processSlashBrackets: 1,
47       processLaTeXenvironments: 0,
48       custom: 0, fixEscapedDollars: 1
49     });
50   },
51   
52   ConvertTeX2: function (element) {
53     this.Convert(element,{
54       processSingleDollars: 0, processDoubleDollars: 1,
55       processSlashParens: 1, processSlashBrackets: 1,
56       processLaTeXenvironments: 0,
57       custom: 0, fixEscapedDollars: 0
58     });
59   },
60   
61   ConvertLaTeX: function (element) {
62     this.Convert(element,{
63       processSingleDollars: 0, processDoubleDollars: 0,
64       processSlashParens: 1, processSlashBrackets: 1,
65       processLaTeXenvironments: 1,
66       custom: 0, fixEscapedDollars: 0
67     });
68   },
69   
70   ConvertCustom: function (element) {
71     this.Convert(element,{custom: 1, fixEscapedDollars: 0});
72   },
73   
74   /*******************************************************************/
75
76   /*
77    *  Define a custom search by indicating the
78    *  strings to use for starting and ending
79    *  in-line and display mathematics
80    */
81   CustomSearch: function (iOpen,iClose,dOpen,dClose) {
82     this.inLineOpen = iOpen; this.inLineClose = iClose;
83     this.displayOpen = dOpen; this.displayClose = dClose;
84     this.createPattern('customPattern',new RegExp(
85       '('+this.patternQuote(dOpen)+'|'
86          +this.patternQuote(iOpen)+'|'
87          +this.patternQuote(dClose)+'|'
88          +this.patternQuote(iClose)+'|\\\\.)','g'
89     ));
90   },
91   
92   patternQuote: function (s) {
93     s = s.replace(/([\^$(){}+*?\-|\[\]\:\\])/g,'\\$1');
94     return s;
95   },
96
97   /*
98    *  MSIE on the Mac doesn't handle lastIndex correctly, so
99    *  override it and implement it correctly.
100    */
101   createPattern: function (name,pattern) {
102     jsMath.tex2math[name] = pattern;
103     if (this.fixPatterns) {
104       pattern.oldExec = pattern.exec;
105       pattern.exec = this.msiePatternExec;
106     }
107   },
108   msiePatternExec: function (string) {
109     if (this.lastIndex == null) (this.lastIndex = 0);
110     var match = this.oldExec(string.substr(this.lastIndex));
111     if (match) {this.lastIndex += match.lastIndex}
112           else {this.lastIndex = null}
113     return match;
114   },
115
116   /*******************************************************************/
117
118   /*
119    *  Set up for the correct type of search, and recursively
120    *  convert the mathematics.  Disable tex2math if the cookie
121    *  isn't set, or of there is an element with ID of 'tex2math_off'.
122    */
123   Convert: function (element,flags) {
124     this.Init();
125     if (!element) {element = jsMath.document.body}
126     if (typeof(element) == 'string') {element = jsMath.document.getElementById(element)}
127     if (jsMath.Controls.cookie.tex2math && 
128         (!jsMath.tex2math.allowDisableTag || !jsMath.document.getElementById('tex2math_off'))) {
129       this.custom = 0; for (var i in flags) {this[i] = flags[i]}
130       if (this.custom) {
131         this.pattern = this.customPattern;
132         this.ProcessMatch = this.customProcessMatch;
133       } else {
134         this.pattern = this.stdPattern;
135         this.ProcessMatch = this.stdProcessMatch;
136       }
137       if (this.processDoubleDollars || this.processSingleDollars ||
138           this.processSlashParens   || this.processSlashBrackets ||
139           this.processLaTeXenvironments || this.custom) this.ScanElement(element);
140     }
141   },
142
143   /*
144    *  Recursively look through a document for text nodes that could
145    *  contain mathematics.
146    */
147   ScanElement: function (element,ignore) {
148     if (!element) {element = jsMath.document.body}
149     if (typeof(element) == 'string') {element = jsMath.document.getElementById(element)}
150     while (element) {
151       if (element.nodeName == '#text') {
152         if (!ignore) {element = this.ScanText(element)}
153       } else {
154         if (element.className == null) {element.className = ''}
155         if (element.firstChild && element.className != 'math') {
156           var off = ignore || element.className.match(/(^| )tex2math_ignore( |$)/) ||
157              (element.tagName && element.tagName.match(/^(script|noscript|style|textarea|pre|code)$/i));
158           off = off && !element.className.match(/(^| )tex2math_process( |$)/);
159           this.ScanElement(element.firstChild,off);
160         }
161       }
162       if (element) {element = element.nextSibling}
163     }
164   },
165   
166   /*
167    *  Looks through a text element for math delimiters and
168    *  process them.  If <BR> tags are found in the middle, they
169    *  are ignored (this is for BBS systems that have editors
170    *  that insert these automatically).
171    */
172   ScanText: function (element) {
173     if (element.nodeValue.replace(/\s+/,'') == '') {return element}
174     var match; var prev; this.search = {};
175     while (element) {
176       this.pattern.lastIndex = 0;
177       while (element && element.nodeName == '#text' &&
178             (match = this.pattern.exec(element.nodeValue))) {
179         this.pattern.match = match;
180         element = this.ProcessMatch(match[0],match.index,element);
181       }
182       if (this.search.matched) {element = this.EncloseMath(element)}
183       if (!element) {return null}
184       prev = element; element = element.nextSibling;
185       while (element && (element.nodeName.toLowerCase() == 'br' ||
186                          element.nodeName.toLowerCase() == "#comment"))
187         {prev = element; element = element.nextSibling}
188       if (!element || element.nodeName != '#text') {return prev}
189     }
190     return element;
191   },
192   
193   /*
194    *  If a matching end tag has been found, process the mathematics.
195    *  Otherwise, update the search data for the given delimiter,
196    *  or ignore it, as the item dictates.
197    */
198   stdProcessMatch: function (match,index,element) {
199     if (match == this.search.end) {
200       this.search.close = element;
201       this.search.cpos = this.pattern.lastIndex;
202       this.search.clength = (match.substr(0,4) == '\\end' ? 0 : match.length);
203       element = this.EncloseMath(element);
204     } else {
205       switch (match) {
206         case '\\(':
207           if ((this.search.end == null ||
208              (this.search.end != '$' && this.search.end != '$$')) &&
209               this.processSlashParens) {
210             this.ScanMark('span',element,'\\)');
211           }
212           break;
213
214         case '\\[':
215           if ((this.search.end == null ||
216              (this.search.end != '$' && this.search.end != '$$')) &&
217                this.processSlashBrackets) {
218             this.ScanMark('div',element,'\\]');
219           }
220           break;
221
222         case '$$':
223           if (this.processDoubleDollars) {
224             var type = (this.doubleDollarsAreInLine? 'span': 'div');
225             this.ScanMark(type,element,'$$');
226           }
227           break;
228
229         case '$':
230           if (this.search.end == null && this.processSingleDollars) {
231             this.ScanMark('span',element,'$');
232           }
233           break;
234
235         case '\\$':
236           if (this.search.end == null && this.fixEscapedDollars) {
237             element.nodeValue = element.nodeValue.substr(0,index)
238                               + element.nodeValue.substr(index+1);
239             this.pattern.lastIndex--;
240           }
241           break;
242           
243         case '\\\\':
244           break;
245
246         default:
247           if (match.substr(0,6) == '\\begin' && this.search.end == null &&
248               this.processLaTeXenvironments) {
249             this.ScanMark('div',element,'\\end'+match.substr(6));
250             this.search.olength = 0;
251           }
252           break;
253       }
254     }
255     return element;
256   },
257
258   /*
259    *  If a matching end tag has been found, process the mathematics.
260    *  Otherwise, update the search data for the given delimiter,
261    *  or ignore it, as the item dictates.
262    */
263   customProcessMatch: function (match,index,element) {
264     if (match == this.search.end) {
265       this.search.close = element;
266       this.search.clength = match.length;
267       this.search.cpos = this.pattern.lastIndex;
268       if (match == this.inLineOpen || match == this.displayOpen) {
269         element = this.EncloseMath(element);
270       } else {this.search.matched = 1}
271     } else if (match == this.inLineOpen) {
272       if (this.search.matched) {element = this.EncloseMath(element)}
273       this.ScanMark('span',element,this.inLineClose);
274     } else if (match == this.displayOpen) {
275       if (this.search.matched) {element = this.EncloseMath(element)}
276       this.ScanMark('div',element,this.displayClose);
277     }
278     return element;
279   },
280
281   /*
282    *  Return a structure that records the starting location
283    *  for the math element, and the end delimiter we want to find.
284    */
285   ScanMark: function (type,element,end) {
286     var len = this.pattern.match[1].length;
287     this.search = {
288       type: type, end: end, open: element, olength: len,
289       pos: this.pattern.lastIndex - len
290     };
291   },
292   
293   /*******************************************************************/
294
295   /*
296    *  Surround the mathematics by an appropriate
297    *  SPAN or DIV element marked as CLASS="math".
298    */
299   EncloseMath: function (element) {
300     var search = this.search; search.end = null;
301     var close = search.close;
302     if (search.cpos == close.length) {close = close.nextSibling}
303        else {close = close.splitText(search.cpos)}
304     if (!close) {close = jsMath.document.createTextNode("")}
305     if (element == search.close) {element = close}
306     var math = search.open.splitText(search.pos);
307     while (math.nextSibling && math.nextSibling != close) {
308       if (math.nextSibling.nodeValue !== null) {
309         if (math.nextSibling.nodeName.toLowerCase() === "#comment") {
310           math.nodeValue += math.nextSibling.nodeValue.replace(/^\[CDATA\[(.*)\]\]$/,"$1");
311         } else {
312           math.nodeValue += math.nextSibling.nodeValue;
313         }
314       } else {
315         math.nodeValue += ' ';
316       }
317       math.parentNode.removeChild(math.nextSibling);
318     }
319     var TeX = math.nodeValue.substr(search.olength,
320       math.nodeValue.length-search.olength-search.clength);
321     math.parentNode.removeChild(math);
322     math = this.createMathTag(search.type,TeX);
323     //
324     //  This is where older, buggy browsers can fail under unpredicatble 
325     //  circumstances, so we trap errors and at least get to continue
326     //  with the rest of the math.  (## should add error message ##)
327     //
328     try {
329       if (close && close.parentNode) {
330         close.parentNode.insertBefore(math,close);
331       } else if (search.open.nextSibling) {
332         search.open.parentNode.insertBefore(math,search.open.nextSibling);
333       } else {
334         search.open.parentNode.appendChild(math);
335       }
336     } catch (err) {}
337     this.search = {}; this.pattern.lastIndex = 0;
338     return math;
339   },
340     
341   /*
342    *  Create an element for the mathematics
343    */
344   createMathTag: function (type,text) {
345     var tag = jsMath.document.createElement(type); tag.className = "math";
346     var math = jsMath.document.createTextNode(text);
347     tag.appendChild(math);
348     return tag;
349   },
350
351   //
352   //  MSIE won't let you insert a DIV within tags that are supposed to
353   //  contain in-line data (like <P> or <SPAN>), so we have to fake it
354   //  using SPAN tags that force the formatting to work like DIV.  We
355   //  use a separate SPAN that is the full width of the containing
356   //  item, and that has the margins and centering from the div.typeset
357   //  style.
358   //
359   MSIEcreateMathTag: function (type,text) {
360     var tag = jsMath.document.createElement("span");
361     tag.className = "math";
362     text = text.replace(/</g,'&lt;').replace(/>/g,'&gt;');
363     if (type == 'div') {
364       tag.className = "tex2math_div";
365       text = '<span class="math">\\displaystyle{'+text+'}</span>';
366     }
367     tag.innerHTML = text;
368     return tag;
369   },
370   
371   /*******************************************************************/
372
373   Init: function () {
374     if (!jsMath.browser && document.all && !window.opera) {
375       jsMath.browser = 'MSIE';
376       jsMath.platform = (navigator.platform.match(/Mac/) ? "mac" :
377                          navigator.platform.match(/Win/) ? "pc" : "unix");
378     }
379     if (this.inited || !jsMath.browser) return;
380     /*
381      *  MSIE can't handle the DIV's properly, so we need to do it by
382      *  hand.  Use an extra SPAN that uses CSS to act like a DIV.
383      */
384     if (jsMath.browser == 'MSIE' && jsMath.platform == 'pc')
385       {this.createMathTag = this.MSIEcreateMathTag}
386     this.inited = 1;
387   },
388   
389   /*
390    *  Test to see if we need to override the pattern exec() call
391    *  (for MSIE on the Mac).
392    */
393   TestPatterns: function () {
394     var pattern = /a/g;
395     var match = pattern.exec("xax");
396     this.fixPatterns = (pattern.lastIndex != 2 && match.lastIndex == 2);
397   }
398   
399 });
400
401 /*
402  *  Initialize
403  */
404 if (jsMath.Controls.cookie.tex2math == null) {jsMath.Controls.cookie.tex2math = 1}
405 if (jsMath.tex2math.allowDisableTag == null) {jsMath.tex2math.allowDisableTag = 1}
406 jsMath.tex2math.TestPatterns();
407 jsMath.tex2math.createPattern('stdPattern',/(\\[\(\)\[\]$\\]|\$\$|\$|\\(begin|end)\{[^}]+\})/g);
408
409 }