Repository: KaTeX/KaTeX (js)

Issue:
\href{} Produces Huge number of elements (1 per character)
Katex is producing huge number of <span> elements when wrapping text within the `\href{}` directive. I can't see any immediate reason to suddenly produce 1 span per character, rather than just simply wrapping the range of identically styled characters in the <a> tag.
[jsfiddle](https://jsfiddle.net/0a7e6fx1/3/)

I am generating a lot of text, and don't want the overhead of adding hundreds of spans for each link.

**(La)TeX code:**
```latex
\text{\href{https://google.com}{abcdefghijk}}
```

**Expected behavior:**
![image](https://user-images.githubusercontent.com/286180/63327182-1276b100-c371-11e9-818f-8b1cb41abdd9.png)

**Environment (please complete the following information):**
 - KaTeX Version: 0.10.2
 - Device: Desktop
 - OS: macOS 10.13.6
 - Browser: Chrome
 - Version: 76



Pull request:
fix(builder): combine characters together in all expressions
An attempt to fix #2079.  Call `tryCombineChars` in `buildExpression`, so all expressions get adjacent characters combined together.

Unlike past KaTeX builds, this happens even in math mode, in particular because `buildExpression` doesn't know (and maybe can't know) the current mode.  This is a significant deviation, so deserves discussion.  Is it bad that we render math-mode `hello` into 4 `mord` spans (`h`, `e`, `ll`, `o`)?  (I believe the multiple `mord`s are caused by the italic correction rule in `tryCombineChars`.)

Alternatively, perhaps we could tell `buildExpression` whether to combine according to the mode... or just review every call to `buildExpression` and see whether to add a call `tryCombineChars`?

Patch:
diff --git a/src/buildCommon.js b/src/buildCommon.js
index 1e05dcd5..3cc07e53 100644
--- a/src/buildCommon.js
+++ b/src/buildCommon.js
@@ -243,6 +243,15 @@ const canCombine = (prev: SymbolNode, next: SymbolNode) => {
         return false;
     }
 
+    // If prev and next both are just "mbin"s or "mord"s we don't combine them
+    // so that the proper spacing can be preserved.
+    if (prev.classes.length === 1) {
+        const cls = prev.classes[0];
+        if (cls === "mbin" || cls === "mord") {
+            return false;
+        }
+    }
+
     for (const style in prev.style) {
         if (prev.style.hasOwnProperty(style)
             && prev.style[style] !== next.style[style]) {
@@ -261,7 +270,7 @@ const canCombine = (prev: SymbolNode, next: SymbolNode) => {
 };
 
 /**
- * Combine consequetive domTree.symbolNodes into a single symbolNode.
+ * Combine consecutive domTree.symbolNodes into a single symbolNode.
  * Note: this function mutates the argument.
  */
 const tryCombineChars = (chars: HtmlDomNode[]): HtmlDomNode[] => {
diff --git a/src/buildHTML.js b/src/buildHTML.js
index 362197cb..a13b28bb 100644
--- a/src/buildHTML.js
+++ b/src/buildHTML.js
@@ -74,6 +74,9 @@ export const buildExpression = function(
         }
     }
 
+    // Combine consecutive domTree.symbolNodes into a single symbolNode.
+    buildCommon.tryCombineChars(groups);
+
     // If `expression` is a partial group, let the parent handle spacings
     // to avoid processing groups multiple times.
     if (!isRealGroup) {
diff --git a/src/functions/op.js b/src/functions/op.js
index 96f6254e..35f99434 100644
--- a/src/functions/op.js
+++ b/src/functions/op.js
@@ -93,8 +93,7 @@ export const htmlBuilder: HtmlBuilderSupSub<"op"> = (grp, options) => {
             base = inner[0];
             base.classes[0] = "mop"; // replace old mclass
         } else {
-            base = buildCommon.makeSpan(
-                ["mop"], buildCommon.tryCombineChars(inner), options);
+            base = buildCommon.makeSpan(["mop"], inner, options);
         }
     } else {
         // Otherwise, this is a text operator. Build the text from the
diff --git a/src/functions/text.js b/src/functions/text.js
index a2d7c803..7acf5434 100644
--- a/src/functions/text.js
+++ b/src/functions/text.js
@@ -63,8 +63,7 @@ defineFunction({
     htmlBuilder(group, options) {
         const newOptions = optionsWithFont(group, options);
         const inner = html.buildExpression(group.body, newOptions, true);
-        return buildCommon.makeSpan(
-            ["mord", "text"], buildCommon.tryCombineChars(inner), newOptions);
+        return buildCommon.makeSpan(["mord", "text"], inner, newOptions);
     },
     mathmlBuilder(group, options) {
         const newOptions = optionsWithFont(group, options);


Test patch:
diff --git a/test/__snapshots__/katex-spec.js.snap b/test/__snapshots__/katex-spec.js.snap
index e5626cb5..8cd15a12 100755
--- a/test/__snapshots__/katex-spec.js.snap
+++ b/test/__snapshots__/katex-spec.js.snap
@@ -413,22 +413,7 @@ exports[`A font parser \\boldsymbol should inherit mbin/mrel from argument 1`] =
             "skew": 0,
             "style": {
             },
-            "text": "+",
-            "width": 0.89444
-          },
-          {
-            "classes": [
-              "mord",
-              "mathbf"
-            ],
-            "depth": 0.13333,
-            "height": 0.63333,
-            "italic": 0,
-            "maxFontSize": 1,
-            "skew": 0,
-            "style": {
-            },
-            "text": "+",
+            "text": "++",
             "width": 0.89444
           }
         ],
@@ -489,21 +474,6 @@ exports[`A font parser \\boldsymbol should inherit mbin/mrel from argument 1`] =
         "attributes": {
         },
         "children": [
-          {
-            "classes": [
-              "mord",
-              "boldsymbol"
-            ],
-            "depth": 0,
-            "height": 0.44444,
-            "italic": 0,
-            "maxFontSize": 1,
-            "skew": 0,
-            "style": {
-            },
-            "text": "x",
-            "width": 0.65903
-          },
           {
             "classes": [
               "mord",
@@ -511,28 +481,13 @@ exports[`A font parser \\boldsymbol should inherit mbin/mrel from argument 1`] =
             ],
             "depth": 0.19444,
             "height": 0.44444,
-            "italic": 0.03704,
-            "maxFontSize": 1,
-            "skew": 0,
-            "style": {
-            },
-            "text": "y",
-            "width": 0.59028
-          },
-          {
-            "classes": [
-              "mord",
-              "boldsymbol"
-            ],
-            "depth": 0,
-            "height": 0.44444,
             "italic": 0.04213,
             "maxFontSize": 1,
             "skew": 0,
             "style": {
             },
-            "text": "z",
-            "width": 0.55509
+            "text": "xyz",
+            "width": 0.65903
           }
         ],
         "classes": [
@@ -1158,10 +1113,7 @@ exports[`Extending katex by new fonts and symbols Add new font class to new exte
                   <span class="sizing reset-size6 size3 mtight">
                     <span class="mord mtight">
                       <span class="mord mockEasternArabicFont-Regular mtight">
-                        ۱
-                      </span>
-                      <span class="mord mockEasternArabicFont-Regular mtight">
-                        ۱
+                        ۱۱
                       </span>
                     </span>
                   </span>