#! /usr/bin/perl

#
#  Creates the optable.js data (for inclusion on jax/element/mml/jax.js)
#  and the optable directory of auxiliary operator data.
#
#  Usage:  ./makeOpTable

open(OPHTML,"Operator Dictionary.html") || die "Can't load operator dictionary";

$lines = join('',<OPHTML>);
$lines =~ s/.*<table class.*<tbody>\s*//is;
$lines =~ s!</tbody>.*!!is;
$lines =~ s/^\s*//gm;
$lines =~ s!(<tr>|</t[dh]>)\n!\1!g;
$lines =~ s!</?tr>!!g;
$lines =~ s!^<th[^>]*>!!gm;
$lines =~ s!</td>$!!gm;
$lines =~ s/&amp;/&/g; $lines =~ s/&amp;/&/g;
$lines =~ s/&lt;/</g; $lines =~ s/&gt;/>/g;

foreach $line (split(/\n/,$lines)) {
  ($id,$char,$name,$form,$prec,$lspace,$rspace,$min,$attributes) = split(/<\/t[dh]><t[dh][^>]*>/,$line);
  next if $id eq "min";
  $op{$form}{$id} = [$lspace,$rspace,$attributes,$name];
}

# hack to add in missing prefix and postfix |

$op{prefix}{"|"} = [0,0,'fence stretchy symmetric','vertical line'];
$op{postfix}{"|"} = [0,0,'fence stretchy symmetric','vertical line'];

@RANGES = (
  ["BasicLatin",0x20,0x7F,'REL'],
  ["Latin1Supplement",0xA0,0xFF,'ORD'],
  ["LatinExtendedA",0x100,0x17F,'ORD'],
  ["LatinExtendedB",0x180,0x24F,'ORD'],
  ["SpacingModLetters",0x2B0,0x2FF,'ORD'],
  ["CombDiacritMarks",0x300,0x36F,'ORD'],
  ["GreekAndCoptic",0x370,0x3FF,'ORD'],
#  ["Cyrillic",0x400,0x4FF,'ORD'],
#  ["PhoneticExtensions",0x1D00,0x1DBF,'ORD'],
  ["LatinExtendedAdditional",0x1E00,0x1EFF,'ORD'],
  ["GeneralPunctuation",0x2000,0x206F,'PUNCT'],
  ["SuperAndSubscripts",0x2070,0x209F,'ORD'],
  ["CurrencySymbols",0x20A0,0x20CF,'ORD'],
  ["CombDiactForSymbols",0x20D0,0x20FF,'ORD'],
  ["LetterlikeSymbols",0x2100,0x214F,'ORD'],
  ["NumberForms",0x2150,0x218F,'ORD'],
  ["Arrows",0x2190,0x21FF,'REL'],
  ["MathOperators",0x2200,0x22FF,'BIN'],
  ["MiscTechnical",0x2300,0x23FF,'ORD'],
#  ["ControlPictures",0x2400,0x243F,'ORD'],
  ["EnclosedAlphanum",0x2460,0x24FF,'ORD'],
#  ["BoxDrawing",0x2500,0x257F,'ORD'],
#  ["BlockElements",0x2580,0x259F,'ORD'],
  ["GeometricShapes",0x25A0,0x25FF,'ORD'],
#  ["MiscSymbols",0x2600,0x26FF,'ORD'],
  ["Dingbats",0x2700,0x27BF,'ORD'],
  ["MiscMathSymbolsA",0x27C0,0x27EF,'ORD'],
  ["SupplementalArrowsA",0x27F0,0x27FF,'REL'],
  ["SupplementalArrowsB",0x2900,0x297F,'REL'],
  ["MiscMathSymbolsB",0x2980,0x29FF,'ORD'],
  ["SuppMathOperators",0x2A00,0x2AFF,'BIN'],
  ["MiscSymbolsAndArrows",0x2B00,0x2BFF,'ORD'],
#  ["CJK",0x3000,0x303F,'ORD'],
#  ["Hiragana",0x3040,0x309F,'ORD'],
#  ["PUA",0xE000,0xFB00,'ORD'],
#  ["AlphaPresentForms",0xFB00,0xFB4F,'ORD'],
#  ["Specials",0xFFF0,0xFFFF,'ORD'],
  ["MathAlphabets",0x1D400,0x1D7FF,'ORD'],
);

sub GetRange {
  my $n = shift;
  if ($n =~ m/^[0-9A-Z]{4}$/) {$n = hex($n)}
  elsif ($n =~ m/^&#x([0-9A-Z]+);/) {$n = hex($1)}
  else {$n = ord(substr($n,0,1))}
  foreach $range (@RANGES) {
    my ($name,$l,$u,$type) = @$range;
    return $name if $l <= $n && $n <= $u;
  }
  return "none";
}

%TEX = (
  '!' => ['CLOSE', '!'],
  '(' => ['OPEN', '('],
  ')' => ['CLOSE', ')'],
  '*' => ['BIN', '*'],
  '2217' => ['BIN', '*'],
  '+' => ['BIN', '+'],
  ',' => ['PUNCT', ','],
  '-' => ['BIN', '-'],
  '2212' => ['BIN', 'minus'],
  '.' => ['ORD', '.'],
  '/' => ['ORD', '/'],
  '2215' => ['BIN','/'],
  ':' => ['REL', ':'],
  ';' => ['PUNCT', ';'],
  '<' => ['REL', '<'],
  '=' => ['REL', '='],
  '>' => ['REL', '>'],
  '?' => ['CLOSE', '?'],
  '[' => ['OPEN', '['],
  ']' => ['CLOSE', ']'],
  '{' => ['OPEN', '{'],
  '}' => ['CLOSE', '}'],
  '|' => [{prefix => 'OPEN', infix => 'ORD', postfix => 'CLOSE'}, '|'],
  '_' => ['ORD', '_'],
  '#' => ['ORD', '#'],
  '$' => ['ORD', '$'],
  '%' => ['ORD', '%'],
  '&' => ['ORD', '&'],
  '^' => ['ORD', '^'],  # accent for MathML
  '~' => ['ORD', '~'],  # accent for MathML
  '`' => ['ORD', '`'],  # accent for MathML

  # Ord symbols
#  '00A7' => ['ORD', '\\S'],     # should not be an <mo>
#  '2135' => ['ORD', '\\aleph'], # should not be an <mo>
#  '210F' => ['ORD', '\\hbar'],  # should not be an <mo>
#  '0131' => ['ORD', '\\imath'], # should not be an <mo>
#  '0237' => ['ORD', '\\jmath'], # should not be an <mo>
  '2113' => ['ORD', '\\ell'],
  '2118' => ['ORD', '\\wp'],
  '211C' => ['ORD', '\\Re'],
  '2111' => ['ORD', '\\Im'],
  '2202' => ['ORD', '\\partial'],
  '221E' => ['ORD', '\\infty'],
  '2032' => ['ORD', '\\prime'],
  '2205' => ['ORD', '\\emptyset'],
  '2207' => ['ORD', '\\nabla'],
  '221A' => ['ORD', '\\surd'],
  '22A4' => ['ORD', '\\top'],
  '22A5' => ['ORD', '\\bot'],
  '2220' => ['ORD', '\\angle'],
  '25B3' => ['ORD', '\\triangle'],
  '\\'   => ['ORD', '\\backslash'],
  '2216' => ['ORD', '\\backslash'],
  '2200' => ['ORD', '\\forall'],
  '2203' => ['ORD', '\\exists'],
  '00AC' => ['ORD', '\\neg'],
  '266D' => ['ORD', '\\flat'],
  '266E' => ['ORD', '\\natural'],
  '266F' => ['ORD', '\\sharp'],
  '2663' => ['ORD', '\\clubsuit'],
  '2662' => ['ORD', '\\diamondsuit'],
  '2661' => ['ORD', '\\heartsuit'],
  '2660' => ['ORD', '\\spadesuit'],

  # Big ops
  '2210' => ['OP', '\\coprod'],
  '22C1' => ['OP', '\\bigvee'],
  '22C0' => ['OP', '\\bigwedge'],
  '2A04' => ['OP', '\\biguplus'],
  '22C2' => ['OP', '\\bigcap'],
  '22C3' => ['OP', '\\bigcup'],
  '222B' => ['OP', '\\int'],
  '220F' => ['OP', '\\prod'],
  '2211' => ['OP', '\\sum'],
  '2A02' => ['OP', '\\bigotimes'],
  '2A01' => ['OP', '\\bigoplus'],
  '2A00' => ['OP', '\\bigodot'],
  '222E' => ['OP', '\\oint'],
  '2A06' => ['OP', '\\bigsqcup'],
  '222B' => ['OP', '\\smallint'],

  # Binary operations
  '25C3' => ['BIN', '\\triangleleft'],
  '25B9' => ['BIN', '\\triangleright'],
  '25B3' => ['BIN', '\\bigtriangleup'],
  '25BD' => ['BIN', '\\bigtriangledown'],
  '2227' => ['BIN', '\\wedge'],
  '2228' => ['BIN', '\\vee'],
  '2229' => ['BIN', '\\cap'],
  '222A' => ['BIN', '\\cup'],
  '2021' => ['BIN', '\\ddagger'],
  '2020' => ['BIN', '\\dagger'],
  '2293' => ['BIN', '\\sqcap'],
  '2294' => ['BIN', '\\sqcup'],
  '228E' => ['BIN', '\\uplus'],
  '2A3F' => ['BIN', '\\amalg'],
  '22C4' => ['BIN', '\\diamond'],
  '2219' => ['BIN', '\\bullet'],
  '2240' => ['BIN', '\\wr'],
  '00F7' => ['BIN', '\\div'],
  '2299' => ['BIN', '\\odot'],
  '2298' => ['BIN', '\\oslash'],
  '2297' => ['BIN', '\\otimes'],
  '2296' => ['BIN', '\\ominus'],
  '2295' => ['BIN', '\\oplus'],
  '2213' => ['BIN', '\\mp'],
  '00B1' => ['BIN', '\\pm'],
  '2218' => ['BIN', '\\circ'],
  '25EF' => ['BIN', '\\bigcirc'],
  '2216' => ['BIN', '\\setminus'],
  '22C5' => ['BIN', '\\cdot'],
  '2217' => ['BIN', '\\ast'],
  '00D7' => ['BIN', '\\times'],
  '22C6' => ['BIN', '\\star'],

  # Relations
  '221D' => ['REL', '\\propto'],
  '2291' => ['REL', '\\sqsubseteq'],
  '2292' => ['REL', '\\sqsupseteq'],
  '2225' => ['REL', '\\parallel'],
  '2223' => ['REL', '\\mid'],
  '22A3' => ['REL', '\\dashv'],
  '22A2' => ['REL', '\\vdash'],
  '2264' => ['REL', '\\leq'],
  '2265' => ['REL', '\\geq'],
  '227B' => ['REL', '\\succ'],
  '227A' => ['REL', '\\prec'],
  '2248' => ['REL', '\\approx'],
#  '227D' => ['REL', '\\succeq'],
#  '227C' => ['REL', '\\preceq'],
  '2AB0' => ['REL', '\\succeq'],
  '2AAF' => ['REL', '\\preceq'],
  '2283' => ['REL', '\\supset'],
  '2282' => ['REL', '\\subset'],
  '2287' => ['REL', '\\supseteq'],
  '2286' => ['REL', '\\subseteq'],
  '2208' => ['REL', '\\in'],
  '220B' => ['REL', '\\ni'],
  '2209' => ['REL', '\\notin'],
  '220B' => ['REL', '\\owns'],
  '226B' => ['REL', '\\gg'],
  '226A' => ['REL', '\\ll'],
  '223C' => ['REL', '\\sim'],
  '2243' => ['REL', '\\simeq'],
  '22A5' => ['REL', '\\perp'],
  '2261' => ['REL', '\\equiv'],
  '224D' => ['REL', '\\asymp'],
  '2323' => ['REL', '\\smile'],
  '2322' => ['REL', '\\frown'],
  '2260' => ['REL', '\\ne'],
  '2245' => ['REL', '\\cong'],
  '2250' => ['REL', '\\doteq'],
  '22C8' => ['REL', '\\bowtie'],
  '22A8' => ['REL', '\\models'],
  '0338' => ['REL', '\\not'],

  # Arrows
  '21D4' => ['REL', '\\Leftrightarrow'],
  '21D0' => ['REL', '\\Leftarrow'],
  '21D2' => ['REL', '\\Rightarrow'],
  '2194' => ['REL', '\\leftrightarrow'],
  '2190' => ['REL', '\\leftarrow'],
  '2192' => ['REL', '\\rightarrow'],
  '21A6' => ['REL', '\\mapsto'],
  '21BC' => ['REL', '\\leftharpoonup'],
  '21BD' => ['REL', '\\leftharpoondown'],
  '21C0' => ['REL', '\\rightharpoonup'],
  '21C1' => ['REL', '\\rightharpoondown'],
  '2197' => ['REL', '\\nearrow'],
  '2198' => ['REL', '\\searrow'],
  '2196' => ['REL', '\\nwarrow'],
  '2199' => ['REL', '\\swarrow'],
  '21CC' => ['REL', '\\rightleftharpoons'],
  '21AA' => ['REL', '\\hookrightarrow'],
  '21A9' => ['REL', '\\hookleftarrow'],
  '27F5' => ['REL', '\\longleftarrow'],
  '27F8' => ['REL', '\\Longleftarrow'],
  '27F6' => ['REL', '\\longrightarrow'],
  '27F9' => ['REL', '\\Longrightarrow'],
  '27FA' => ['REL', '\\Longleftrightarrow'],
  '27F7' => ['REL', '\\longleftrightarrow'],
  '27FC' => ['REL', '\\longmapsto'],

  # Dots
  '2026' => ['INNER', '\\ldots'],
  '22EF' => ['INNER', '\\cdots'],
  '22EE' => ['ORD',   '\\vdots'],
  '22F1' => ['INNER', '\\ddots'],
  '002E' => ['PUNCT', '\\ldotp','separator'],
# '22C5' => ['PUNCT', '\\cdotp'],  # already given above
# '003A' => ['PUNCT', '\\colon'],  # already given in dictionary

  # Over-under
  '203E' => ['ORD', '\\overline','stretchy'],
  '23AF' => ['ORD', '\\underline','stretchy'],
  '23DE' => ['ORD', '\\overbrace','stretchy'],
  '23DF' => ['ORD', '\\underbrace','stretchy'],

  # Accents
  '0301' => ['ORD', '\\acute','accent'],
  '0300' => ['ORD', '\\grave','accent'],
  '0308' => ['ORD', '\\ddot','accent'],
  '0303' => ['ORD', '\\tilde','accent stretchy'],
  '0304' => ['ORD', '\\bar','accent'],
  '0306' => ['ORD', '\\breve','accent'],
  '030C' => ['ORD', '\\check','accent'],
  '0302' => ['ORD', '\\hat','accent stretchy'],
  '20D7' => ['ORD', '\\vec','accent'],
  '0307' => ['ORD', '\\dot','accent'],

  '02CA' => ['ORD', '\\acute','accent'],
  '02CB' => ['ORD', '\\grave','accent'],
  '00A8' => ['ORD', '\\ddot','accent'],
  '02DC' => ['ORD', '\\tilde','accent stretchy'],
  '02C9' => ['ORD', '\\bar','accent'],
  '02D8' => ['ORD', '\\breve','accent'],
  '02C7' => ['ORD', '\\check','accent'],
  '02C6' => ['ORD', '\\hat','accent stretchy'],
  '02D9' => ['ORD', '\\dot','accent'],

  # Delimiters
  '2191' => ['REL', '\\uparrow'],
  '2193' => ['REL', '\\downarrow'],
  '2195' => ['REL', '\\updownarrow'],
  '21D1' => ['REL', '\\Uparrow'],
  '21D3' => ['REL', '\\Downarrow'],
  '21D5' => ['REL', '\\Updownarrow'],
  '2308' => ['OPEN',  '\\lceil'],
  '2309' => ['CLOSE', '\\rceil'],
  '230A' => ['OPEN',  '\\lfloor'],
  '230B' => ['CLOSE', '\\rfloor'],
  '23AA' => ['ORD',   '\\bracevert'],
  '23B0' => ['OPEN',  '\\lmoustache','fence stretchy symmetric'],
  '23B1' => ['CLOSE', '\\rmoustache','fence stretchy symmetric'],
  '27E8' => ['OPEN',  '\\langle'],
  '27E9' => ['CLOSE', '\\rangle'],
  '27EE' => ['OPEN',  '\\lgroup','fence stretchy symmetric'],
  '27EF' => ['CLOSE', '\\rgroup','fence stretchy symmetric'],

  # invisibles
  '2061' => ['ORD','function application'],
  '2062' => ['ORD','invisible times'],
  '2063' => ['ORD','invisible separator','separator'],
  '2064' => ['ORD','invisible plus'],


  # aliases
  '00AF' => ['ORD','horizontal line','accent stretchy'],
  '2015' => ['ORD','horizontal line','stretchy'],
  '2017' => ['ORD','horizontal line','stretchy'],
  '0332' => ['ORD','horizontal line','accent stretchy'],
  '2329' => ['OPEN','langle','fence stretchy symmetric'],
  '232A' => ['CLOSE','rangle','fence stretchy symmetric'],
  '2500' => ['ORD','horizontal line'],
  '2758' => ['REL','vertical separator','stretchy symmetric'],
  '3008' => ['OPEN','langle','fence stretchy symmetric'],
  '3009' => ['CLOSE','rangle','fence stretchy symmetric'],
  'FE37' => ['ORD','horizontal brace down','accent stretchy'],
  'FE38' => ['ORD','horizontal brace up','accent stretchy'],

  '00B7' => ['BIN','center dot'],
  '02B9' => ['ORD','prime'],
  '2022' => ['BIN','bullet'],
  '2044' => ['BIN','fraction slash'],
  '2305' => ['BIN','barwedge'],
  '2306' => ['BIN','doublebarwedge'],
  '25B5' => ['BIN','triangle'],
  '25BF' => ['BIN','triangledown'],
  '2A2F' => ['BIN','cross product'],

);

%LSPACE = (ORD => 0, OP => 0, BIN => 3, REL => 4, OPEN => 0, CLOSE => 0, PUNCT => 0);
%RSPACE = (ORD => 0, OP => 0, BIN => 3, REL => 4, OPEN => 0, CLOSE => 0, PUNCT => 3);

@CLASS = (
  '[0,0,TEXCLASS.ORD]' => 'ORD',
  '[1,1,TEXCLASS.ORD]' => 'ORD11',
  '[2,1,TEXCLASS.ORD]' => 'ORD21',
  '[0,2,TEXCLASS.ORD]' => 'ORD02',
  '[5,5,TEXCLASS.ORD]' => 'ORD55',
  '[1,2,TEXCLASS.OP,{largeop: true, movablelimits: true, symmetric: true}]' => 'OP',
  '[1,2,TEXCLASS.OP,{largeop: true, movablelimits: true}]' => 'OPFIXED',
  '[0,1,TEXCLASS.OP,{largeop: true, symmetric: true}]' => 'INTEGRAL',
  '[1,2,TEXCLASS.OP,{largeop: true, symmetric: true}]' => 'INTEGRAL2',
  '[3,3,TEXCLASS.BIN]' => 'BIN3',
  '[4,4,TEXCLASS.BIN]' => 'BIN4',
  '[0,1,TEXCLASS.BIN]' => 'BIN01',
  '[4,4,TEXCLASS.BIN,{stretchy: true}]' => 'TALLBIN',
  '[4,4,TEXCLASS.BIN,{largeop: true, movablelimits: true}]' => 'BINOP',
  '[5,5,TEXCLASS.REL]' => 'REL',
  '[1,1,TEXCLASS.REL,{stretchy: true}]' => 'REL1',
  '[4,4,TEXCLASS.REL]' => 'REL4',
  '[5,5,TEXCLASS.REL,{stretchy: true}]' => 'WIDEREL',
  '[5,5,TEXCLASS.REL,{accent: true, stretchy: true}]' => 'RELACCENT',
  '[0,0,TEXCLASS.OPEN,{fence: true, stretchy: true, symmetric: true}]' => 'OPEN',
  '[0,0,TEXCLASS.CLOSE,{fence: true, stretchy: true, symmetric: true}]' => 'CLOSE',
  '[0,0,TEXCLASS.INNER]' => 'INNER',
  '[0,3,TEXCLASS.PUNCT]' => 'PUNCT',
  '[0,0,TEXCLASS.ORD,{accent: true}]' => 'ACCENT',
  '[0,0,TEXCLASS.ORD,{accent: true, stretchy: true}]' => 'WIDEACCENT',
);

while (@CLASS) {
  my $def = shift(@CLASS), $name = shift(@CLASS);
  $CLASS{$def} = $name;
  $CLASSDEF{$name} = $def;
  push(@CLASSES,$name);
}

foreach $form (reverse(sort(keys %op))) {
  foreach $id (sort(keys %{$op{$form}})) {
    $ID = $id; $UID = $id; $UID =~ s/([\\'])/\\\1/g;
    if ($ID =~ m/^&#x([0-9A-F]+);$/i) {$ID = $1; while (length($ID) < 4) {$ID = "0$ID"}; $UID = "\\u$ID"}
    $UID =~ s/&#x([0-9A-F]{4});/\\u\1/g; $UID =~ s/&#x([0-9A-F]{3});/\\u0\1/g;
    ($lspace,$rspace,$attributes,$name) = @{$op{$form}{$id}};
    $class = ($TEX{$ID} ? $TEX{$ID}[0] : undef);
    $class = $class->{$form} if ref($class) eq 'HASH';
    if (!($hasClass = defined($class))) {
      $class = "ORD";
      $class = "REL" if $lspace == 5;
      $class = "BIN" if $lspace == 4 || $lspace == 3;
      $class = "ORD" if $lspace == 0 && $rspace == 0;
      $class = "OPEN" if $attributes =~ m/fence/ && $name =~ m/open|left/i;
      $class = "CLOSE" if $attributes =~ m/fence/ && $name =~ m/close|right/i;
      $class = "OP" if $attributes =~ m/largeop/;
      $class = "BIN" if length($ID) == 2;
    }
    @DATA = ($lspace,$rspace,"TEXCLASS.$class");
    push(@DATA,"{".join(': true, ',sort(split(/ /,$attributes))).": true}") if $attributes;
    $DATA = "[".join(",",@DATA)."]";
    $DATA = "MO.$CLASS{$DATA}" if defined $CLASS{$DATA};
    $DATA = sprintf("%-30s","        '$UID': $DATA,")." // $name\n";
    $USED{$ID} = 1;
    if ($hasClass) {$unicode = "Main"} else {$unicode = GetRange($ID)}
    $range{$unicode} = {} unless defined $range{$unicode};
    $range{$unicode}{$form} = [] unless defined $range{$unicode}{$form};
    push(@{$range{$unicode}{$form}},$DATA);
  }
  if ($form eq 'infix') {
    foreach $ID (sort(keys %TEX)) {
      unless ($USED{$ID}) {
	next if $onlyTeX && !$TEX{$ID};
	@DATA = ($LSPACE{$TEX{$ID}[0]},$RSPACE{$TEX{$ID}[0]},"TEXCLASS.$TEX{$ID}[0]");
	push(@DATA,"{".join(': true, ',sort(split(/ /,$TEX{$ID}[2]))).": true}") if $TEX{$ID}[2];
	$DATA = "[".join(",",@DATA)."]";
	$DATA = "MO.$CLASS{$DATA}" if defined $CLASS{$DATA};
	$UID = $ID; $UID = "\\u$ID" if length($UID) == 4;
	$DATA = sprintf("%-30s","        '$UID': $DATA,")." // $TEX{$ID}[1]\n";
	$range{Main} = {} unless defined $range{Main};
	$range{Main}{infix} = [] unless defined $range{Main}{infix};
	push(@{$range{Main}{infix}},$DATA);
      }
    }
  }
}

mkdir "optable" unless -e "optable";

@RANGEDEF = ();
foreach $def (@RANGES) {
  my ($name,$l,$u,$class) = @$def;
  if (defined $range{$name}) {
    push(@RANGEDEF,"[".sprintf("0x%X,0x%X",$l,$u).",TEXCLASS.$class,\"$name\"]");
  } elsif ($class ne 'REL') {
    push(@RANGEDEF,"[".sprintf("0x%X,0x%X",$l,$u).",TEXCLASS.$class]");
  }
}

foreach $unicode (sort(keys %range)) {
  print "$unicode...";
  @forms = ();
  if ($unicode eq "Main") {
    foreach $form (reverse(sort(keys %{$range{$unicode}}))) {
      $lines = $range{$unicode}{$form};
      $lines->[scalar(@$lines)-1] =~ s!\, ( *)//!  \1//!;
      push(@forms,"      $form: {\n".join("",@$lines)."      }");
    }
    open(JSFILE,">optable.js") || die "Can't write optable.js";
    print JSFILE "  var MO = {\n";
    print JSFILE "    ",join(",\n    ",map {sprintf("%-11s",$_.":")." ".$CLASSDEF{$_}} @CLASSES),"\n";
    print JSFILE "  };\n\n";
    print JSFILE "  MML.mo.Augment({\n";
    print JSFILE "    SPACE: [\n";
    print JSFILE "      '0em',\n";
    print JSFILE "      '0.1111em',\n";
    print JSFILE "      '0.1667em',\n";
    print JSFILE "      '0.2222em',\n";
    print JSFILE "      '0.2667em',\n";
    print JSFILE "      '0.3333em'\n";
    print JSFILE "    ],\n";
    print JSFILE "    RANGES: [\n";
    print JSFILE "      ",join(",\n      ",@RANGEDEF),"\n";
    print JSFILE "    ],\n";
    print JSFILE "    OPTABLE: {\n";
    print JSFILE join(",\n",@forms),"\n";
    print JSFILE "    }\n";
    print JSFILE "  },{\n";
    print JSFILE "    OPTYPES: MO\n";
    print JSFILE "  });\n";
    close(JSFILE);
  } else {
    foreach $form (reverse(sort(keys %{$range{$unicode}}))) {
      $lines = $range{$unicode}{$form};
      $lines->[scalar(@$lines)-1] =~ s!, ( *)//!  \1//!;
      push(@forms,"      $form: {\n".join("",@$lines)."      }");
    }
    open(JSFILE,">optable/$unicode.js") || die "Can't write optable/$unicode.js";
    print JSFILE "/*************************************************************\n";
    print JSFILE " *\n";
    print JSFILE " *  MathJax/jax/output/HTML-CSS/optable/$unicode.js\n";
    print JSFILE " *\n";
    print JSFILE " *  Copyright (c) 2010 Design Science, Inc.\n";
    print JSFILE " *\n";
    print JSFILE " *  Licensed under the Apache License, Version 2.0 (the \"License\");\n";
    print JSFILE " *  you may not use this file except in compliance with the License.\n";
    print JSFILE " *  You may obtain a copy of the License at\n";
    print JSFILE " *\n";
    print JSFILE " *     http://www.apache.org/licenses/LICENSE-2.0\n";
    print JSFILE " *\n";
    print JSFILE " *  Unless required by applicable law or agreed to in writing, software\n";
    print JSFILE " *  distributed under the License is distributed on an \"AS IS\" BASIS,\n";
    print JSFILE " *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n";
    print JSFILE " *  See the License for the specific language governing permissions and\n";
    print JSFILE " *  limitations under the License.\n";
    print JSFILE " *\n";
    print JSFILE " */\n\n";
    print JSFILE "(function (MML) {\n";
    print JSFILE "  var MO = MML.mo.OPTYPES;\n";
    print JSFILE "  var TEXCLASS = MML.TEXCLASS;\n\n";
    print JSFILE "  MathJax.Hub.Insert(MML.mo.prototype,{\n";
    print JSFILE "    OPTABLE: {\n";
    print JSFILE join(",\n",@forms),"\n";
    print JSFILE "    }\n";
    print JSFILE "  });\n\n";
    print JSFILE "  MathJax.Ajax.loadComplete(MML.optableDir+\"/$unicode.js\");\n\n";
    print JSFILE "})(MathJax.ElementJax.mml);\n";
    close(JSFILE);
  }
  print "\n";
}
