Differ

MiniLatex uses a number of optimizations. One of these is to re-parse and re-render only paragraphs which have changed. This is optimization is important for the editing process, since instantaneous feedback is essential for a satisfying user experience. Such differential transformations are more important for parsing than for rendering, since parsing is by far the most expensive process in terms of execution time.

The strategy is as follows. Let u and v be two lists of strings. Write them as u = a x b, v = u y b, where a is the greatest common prefix and b is the greatest common suffix. Supposed that we have in hand u' = render u = a' x' b', where a' = render a, etc. Let y' = render y. Then v' = diff u = a' y' b'.

The strategy just outlined is far from optimal for arbitrary changes to the list v. However, a real human editor generally makes small, localized changes to v, so in "real life," the strategy is near-optimal.

NOTE. I would like to thank Ilias van Peer, who recommended using a diffing strategy for performance optimization.

Implementing the diff stategy

The stuctures u = a x b, v = u y b map directly onto the DiffRecord type:

type alias DiffRecord =
    { commonInitialSegment : List String
    , commonTerminalSegment : List String
    , middleSegmentInSource : List String
    , middleSegmentInTarget : List String
    }

The implementation of

diff : List String -> List String -> DiffRecord

is straightforward.

Edit records

The function diff carries out the transformation a x b => a y b. To effect the transformation a' x' b' => a' y' b', one needs the notion of an EditRecord, which carries the needed state:

type alias EditRecord a =
    { paragraphs : List String
    , renderedParagraphs : List a
    , latexState : LatexState
    , idList : List String
    }

The rendering function applied to an EditRecord takes paragraphs and LatexState as arguments and returns renderedParagraphs. The idList is a list of ids, one for each rendered paragraph. When an updated EditRecord is formed, the ids of the changed paragraphs are changed. This is done in order to help both the Elm renderer and MathJax to be as efficient as possible. Again: speed optimizations. The initial ids are like this p.0.1, p.0.2, p.0.3, etc. That is, they are sequential in the third component. Suppose that the paragraphs with ids p.0.17 and p.0.18 are changed. The corresponding ids in the new rendered text are something like p.3785.17 and p.3785.18, where 3785 is randomly generated.

Differential rendering

Below is the definition of differentialRender. It takes a rendering function, a DiffRecord, and an EditRecord as input and produces a list of rendered paragraphs as output.

Differ.differentialRender : (String -> a) -> DiffRecord -> EditRecord a -> List a
Differ.differentialRender renderer diffRecord editRecord =
    let
        ii =
            List.length diffRecord.commonInitialSegment

        it =
            List.length diffRecord.commonTerminalSegment

        initialSegmentRendered =
            List.take ii editRecord.renderedParagraphs

        terminalSegmentRendered =
            takeLast it editRecord.renderedParagraphs

        middleSegmentRendered =
            List.map renderer diffRecord.middleSegmentInTarget
    in
    initialSegmentRendered ++ middleSegmentRendered ++ terminalSegmentRendered

Updating an EditRecord

Updating an EditRecord is a multistep process. The Differ.update function takes as input a random number seed, a rendering function, and EditRecord, and a string of source text. The output is an updated EditRecord. Here are the steps:

  1. Apply logicalParagraphify to transform the input source text string into a list of logical paragraphs.

  2. Apply diff to the "old paragraphs" in the EditRecord and the "new paragraphs" from (1) to produce a DiffRecord.

  3. Apply differentialRender to the DiffRecord from step (2) and the list of logical paragraphs frome step (1) to producn updated list of rendererd paragraphs.

  4. Apply differentialIdList to the random seed, the DiffRecord, and "old" idList from the EditRecord to create a new list of paragraph ids.

The final step is to used the information harvested in 1--4 to create a new EditRecord.

Differ.update : Int -> (String -> a) -> EditRecord a -> String -> EditRecord a
Differ.update seed transformer editRecord text =
    let
        newParagraphs =
            Paragraph.logicalParagraphify text

        diffRecord =
            diff editRecord.paragraphs newParagraphs

        newRenderedParagraphs =
            differentialRender transformer diffRecord editRecord

        p =
            differentialIdList seed diffRecord editRecord
    in
    EditRecord newParagraphs newRenderedParagraphs emptyLatexState p.idList

Last updated