Skip to content

Fragments and Selections

Han Wei edited this page Jun 19, 2012 · 1 revision

This is mainly about v0.3, as of this writing the dev branch of MathQuill, but also has notes on v0.2, the current master branch.

A MathFragment is an entity outside the math DOM tree with one-way pointers (so it's only a "view" of part of the tree, not an actual node/entity in the tree) that delimit a list of MathCmds, i.e., symbols and operators.

It's used in one main way and a few others:

  • its most important uses are instances of its subclass Selection, which the cursor creates whenever it selects something
  • it is also used when LiveFraction takes stuff that was behind it when typed and puts it into its first block, e.g. when typing 1/2
  • and when TextBlock splits apart and puts one piece (a fragment, you might say) of its text in another TextBlock

It's creation is straightforward, just pass the first and last elements to the constructor (which is the same for both). Selection has methods that manipulate it (.{extend,retract}{Left,Right}(), .levelUp(), .clear()) that only the cursor calls, but to actually use it you have to call .detach() or .blockify(), whose use is a little..."tricky" to understand.

.blockify() is stupid, I don't know what I was thinking. Well, I know exactly what I was thinking, and I was being stupid. It's there because in the beginning, before TextBlock existed, there was no separate MathFragment and Selection, Selection had all the code now currently in MathFragment and the relevant pieces that LiveFraction used were just duplicated in its .createBefore() method (it wasn't called that at the time, but that's essentially what the method was). We thought of Selection as an extension of Cursor, so instead of .first and .last, it had .parent, .prev, .next, just like the cursor, only without the constraint that prev.next === next && next.prev === prev. (This is still true in master.)

Now, the natural thing to do with a fragment of the tree when, say, replacing a selection with its square root, is to detach its contents and pass it into the square root. But you don't detach the .prev and .next, and the MathFragment only has indirect access to its contents, so once the contents have been detached from between the .prev and .next, well, you have no pointer to any of the contents to put into the square root anymore. Rats.

So, we should use .first and .last, right? Not so fast! Ignore the obvious solution, that's not complicated enough. If you think about it, but not too hard, the (at the time) two use cases of MathFragment, LiveFraction eating previous commands and replacing a selection, both culminate in a new MathBlock whose entire contents are the contents of the fragment. You thinking what I'm thinking? Liar, I'm thinking "you have no idea what I'm thinking".

So now we just have .createBefore() check if there's a replacedFragment (which would be the selection we're replacing), and if so, call .blockify(), which gives you a new MathBlock to use as the first block in the command. (MathCmds without blocks are Symbols, whose .replaces() method, instead of setting .replacedFragment, removes that MathFragment.)

But wait, there's more! Turns out it's not true that all we do with MathFragments is .blockify() them, when LatexCommandInputs replace some selected math, like when replacing with a square root, for example, first you have to type \sqrt, during which the LatexCommandInput never had its contents set to the selected math that you're replacing, instead, the LatexCommandInput holds on to the selection until you're done entering your LaTeX command, at which point it hands the selection over to the command you entered to be .blockify()-ed (or removed). So we also need a .detach() method for LatexCommandInput to call that will remove it from the math tree, so when you move the cursor it moves around the detached selection, but is still visible, so you know that typing backslash didn't just completely eat your selected math. Note that upcoming in v0.3, we're moving LatexCommandInput from an independant command to being part of cursor, and it will not be possible to edit math while in a LatexCommandInput, which simplifies things.

Now, if .blockify() is called without .detach() being called, it has to do everything .detach() does, namely re-arranging the pointers from surrounding math to the contents of the selection to point around the selection, but if .blockify() is called after .detach() being called, we can't have it doing all those things again, especially since the surrounding math could have changed (until we make it so you can't edit math while in the LatexCommandInput). We also can't put it in .blockify()'s contract that .detach() has to be called first, because that would be too complicated compared to just using self-modifying code to guarantee that .detach() is idemptotent. (Sound similar to any recent proposals for MathElement UUIDs? Happens to the best of us.) Specifically, .detach(), after doing its thing, then sets this.detach to be a no-op, so that if .detach() is called again on that instance of MathFragment, it won't try to do its thing again. Then we can just have .blockify() call .detach() first, and if .detach() was called by LatexCommandInput way earlier, it knows that and does nothing, if not, does its thing now.

One more tricky bit is that Selection has a containing span colored blue, which a MathFragment such as created by LiveFraction wouldn't want, so a normal MathFragment's .jQ is just a jQuery set that's the union of all the contents' jQuery sets, while the Selection's .jQ is the containing span. In its constructor the Selection creates the span and wraps its contents in it, and on blockification it unwraps the contents, removing the span and resetting the .jQ. So, in MathCmd::createBlocks(), we can be sure that upon calling .blockify(), the .jQ of the block will be the union of the contents' jQuery sets.

In the html branch, I do .blockify() to guarantee the state of the .jQ and temporarily store the block and put its contents (both math and jQuery) into that of the first block. In the new architecture, MathFragment's equivalent will have methods to insert its contents into places.

(To make sure, I grepped for all uses of MathFragment or Selection's .jQ, it's only touched by:

  • MathFragment and Selection's methods, of course
    • the constructors set .jQ
      • MathFragment's sets it to be the union of the contents' jQuery sets
      • Selection's sets it to be the blue wrapper span
    • MathFragment::remove() does .jQ.remove()
    • MathFragment::blockify() sets the new MathBlock's .jQ to be that of the fragment
      • it later will be inserted into the .jQ created for the MathBlock
      • Selection::blockify() unwraps the blue wrapper span first
    • Selection::levelUp(), clear(), {extend,retract}{Left,Right}() do their thing to it, of course
  • Cursor::show(), which inserts the cursor's jQ before it
  • the textarea focus/blur handlers, which gray and ungray it
  • TextBlock::replaces(), which calls .text() on it
  • LatexCommandInput, which grays out the selection and prevents mouse events from putting the cursor in the detached selection)
Clone this wiki locally