Login
fsl_diff.c at [ea8eb63bb7]
Login

File src/fsl_diff.c artifact 8d947bd239 part of check-in ea8eb63bb7


/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*
** Copyright (c) 2013 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)
**
** This program is distributed in the hope that it will be useful,
** but without any warranty; without even the implied warranty of
** merchantability or fitness for a particular purpose.
**
** Author contact information:
**   drh@hwaci.com
**   http://www.hwaci.com/drh/
**
*******************************************************************************
** This file houses Fossil's diff-generation routines (as opposed to
** the delta-generation).
*/
#include "fossil-scm/fossil.h"
#include <assert.h>
#include <memory.h>
#include <stdlib.h>

typedef fsl_uint64_t u64;
typedef void ReCompiled /* porting crutch. i would strongly prefer to
                           replace the regex support with a stateful
                           predicate callback.
                        */;
#define TO_BE_STATIC /* Porting crutch for unused static funcs */

#define DIFF_CONTEXT_MASK ((u64)0x0000ffff) /* Lines of context. Default if 0 */
#define DIFF_WIDTH_MASK   ((u64)0x00ff0000) /* side-by-side column width */
#define DIFF_IGNORE_EOLWS ((u64)0x01000000) /* Ignore end-of-line whitespace */
#define DIFF_SIDEBYSIDE   ((u64)0x02000000) /* Generate a side-by-side diff */
#define DIFF_VERBOSE      ((u64)0x04000000) /* Missing shown as empty files */
#define DIFF_BRIEF        ((u64)0x08000000) /* Show filenames only */
#define DIFF_INLINE       ((u64)0x00000000) /* Inline (not side-by-side) diff */
#define DIFF_HTML         ((u64)0x10000000) /* Render for HTML */
#define DIFF_LINENO       ((u64)0x20000000) /* Show line numbers */
#define DIFF_WS_WARNING   ((u64)0x40000000) /* Warn about whitespace */
#define DIFF_NOOPT        (((u64)0x01)<<32) /* Suppress optimizations (debug) */
#define DIFF_INVERT       (((u64)0x02)<<32) /* Invert the diff (debug) */
#define DIFF_CONTEXT_EX   (((u64)0x04)<<32) /* Use context even if zero */
#define DIFF_NOTTOOBIG    (((u64)0x08)<<32) /* Only display if not too big */

/**
 ** Maximum length of a line in a text file, in bytes.  (2**13 = 8192 bytes)
 */
#define LENGTH_MASK_SZ  13
#define LENGTH_MASK     ((1<<LENGTH_MASK_SZ)-1)


/**
 ** Extract the number of lines of context from diffFlags.  Supply an
 ** appropriate default if no context width is specified.
 */
static int diff_context_lines(fsl_uint64_t diffFlags){
  int n = diffFlags & DIFF_CONTEXT_MASK;
  if( n==0 && (diffFlags & DIFF_CONTEXT_EX)==0 ) n = 5;
  return n;
}

/*
** Extract the width of columns for side-by-side diff.  Supply an
** appropriate default if no width is given.
*/
static int diff_width(fsl_uint64_t diffFlags){
  int w = (diffFlags & DIFF_WIDTH_MASK)/(DIFF_CONTEXT_MASK+1);
  if( w==0 ) w = 80;
  return w;
}

/**
 ** Converts mask of public fsl_diff_flag_t (32-bit) values to the
 ** Fossil-internal 64-bit bitmask used by the DIFF_xxx macros. Why? 
 ** (A) fossil(1) uses the macro approach and a low-level encoding of
 ** data in the bitmask (e.g. the context lines count). The public API
 ** exposes hides the lower-level flags and allow the internal API to
 ** take care of the encoding.
 */
static fsl_uint64_t fsl_diff_flags_convert( int mask ){
  fsl_uint64_t rc = 0U;
#define DO(F) if(mask & F) rc |= (((u64)F) << 24)
  DO(FSL_DIFF_IGNORE_EOLWS);
  DO(FSL_DIFF_SIDEBYSIDE);
  DO(FSL_DIFF_VERBOSE);
  DO(FSL_DIFF_BRIEF);
  DO(FSL_DIFF_HTML);
  DO(FSL_DIFF_LINENO);
  DO(FSL_DIFF_WS_WARNING);
  DO(FSL_DIFF_NOOPT);
  DO(FSL_DIFF_INVERT);
  DO(FSL_DIFF_CONTEXT_EX);
  DO(FSL_DIFF_NOTTOOBIG);
#undef DO
  return rc;
}

/*
** Information about each line of a file being diffed.
**
** The lower LENGTH_MASK_SZ bits of the hash (DLine.h) are the length
** of the line.  If any line is longer than LENGTH_MASK characters,
** the file is considered binary.
*/
typedef struct DLine DLine;
struct DLine {
  const char *z;        /* The text of the line */
  unsigned int h;       /* Hash of the line */
  unsigned int iNext;   /* 1+(Index of next line with same the same hash) */

  /* an array of DLine elements serves two purposes.  The fields
  ** above are one per line of input text.  But each entry is also
  ** a bucket in a hash table, as follows: */
  unsigned int iHash;   /* 1+(first entry in the hash chain) */
};

/*
** Length of a dline
*/
#define LENGTH(X)   ((X)->h & LENGTH_MASK)

/*
** Return an array of DLine objects containing a pointer to the
** start of each line and a hash of that line.  The lower
** bits of the hash store the length of each line.
**
** Trailing whitespace is removed from each line.  2010-08-20:  Not any
** more.  If trailing whitespace is ignored, the "patch" command gets
** confused by the diff output.  Ticket [a9f7b23c2e376af5b0e5b]
**
** Return 0 if the file is binary or contains a line that is
** too long.
**
** Profiling show that in most cases this routine consumes the bulk of
** the CPU time on a diff.
*/
static int break_into_lines(const char *z, int n, int *pnLine,
                                  DLine **pOut, int ignoreWS){
  int nLine, i, j, k, x;
  unsigned int h, h2;
  DLine *a;

  /* Count the number of lines.  Allocate space to hold
  ** the returned array.
  */
  for(i=j=0, nLine=1; i<n; i++, j++){
    int c = z[i];
    if( c==0 ){
      *pOut = NULL;
      return FSL_RC_TYPE;
    }
    if( c=='\n' && z[i+1]!=0 ){
      nLine++;
      if( j>LENGTH_MASK ){
        return FSL_RC_RANGE;
      }
      j = 0;
    }
  }
  if( j>LENGTH_MASK ){
    return FSL_RC_RANGE;
  }
  a = fsl_malloc( nLine*sizeof(a[0]) );
  if(!a) return FSL_RC_OOM;
  memset(a, 0, nLine*sizeof(a[0]) );
  if( n==0 ){
    *pnLine = 0;
    *pOut = a;
    return 0;
  }

  /* Fill in the array */
  for(i=0; i<nLine; i++){
    a[i].z = z;
    for(j=0; z[j] && z[j]!='\n'; j++){}
    k = j;
    while( ignoreWS && k>0 && fsl_isspace(z[k-1]) ){ k--; }
    for(h=0, x=0; x<=k; x++){
      h = h ^ (h<<2) ^ z[x];
    }
    a[i].h = h = (h<<LENGTH_MASK_SZ) | k;;
    h2 = h % nLine;
    a[i].iNext = a[h2].iHash;
    a[h2].iHash = i+1;
    z += j+1;
  }

  /* Return results */
  *pnLine = nLine;
  *pOut = a;
  return 0;
}


/*
** Return true if two DLine elements are identical.
*/
static int same_dline(DLine *pA, DLine *pB){
  return pA->h==pB->h && memcmp(pA->z,pB->z,pA->h & LENGTH_MASK)==0;
}


/*
** A context for running a raw diff.
**
** The aEdit[] array describes the raw diff.  Each triple of integers in
** aEdit[] means:
**
**   (1) COPY:   Number of lines aFrom and aTo have in common
**   (2) DELETE: Number of lines found only in aFrom
**   (3) INSERT: Number of lines found only in aTo
**
** The triples repeat until all lines of both aFrom and aTo are accounted
** for.
*/
typedef struct DContext DContext;
struct DContext {
  int *aEdit;        /* Array of copy/delete/insert triples */
  int nEdit;         /* Number of integers (3x num of triples) in aEdit[] */
  int nEditAlloc;    /* Space allocated for aEdit[] */
  DLine *aFrom;      /* File on left side of the diff */
  int nFrom;         /* Number of lines in aFrom[] */
  DLine *aTo;        /* File on right side of the diff */
  int nTo;           /* Number of lines in aTo[] */
};

/**
 ** Holds output state for diff generation.
 */
struct DiffOutState {
  /** Output callback. */
  fsl_output_f out;
  /** State for this->out(). */
  void * oState;
  /** For propagating output errors. */
  int rc;
};
typedef struct DiffOutState DiffOutState;
static const DiffOutState DiffOutState_empty = {NULL, NULL, 0};

/**
 ** Internal helper. Sends src to o->out(). If n is negative, fsl_strlen()
 ** is used to determine the length.
 */
static int diff_out( DiffOutState * o, void const * src, fsl_int_t n ){
  return o->rc = n
    ? o->out(o->oState, src, (n<0)?fsl_strlen((char const *)src):(fsl_size_t)n)
    : 0;
}


/**
 ** fsl_appendf_f() impl for use with diff_outf(). state must be a
 ** (DiffOutState*).
 */
static fsl_int_t fsl_appendf_f_diff_out( void * state, char const * s,
                                         fsl_int_t n ){
  DiffOutState * os = (DiffOutState *)state;
  diff_out(os, s, n);
  return os->rc ? -1 : n;
}

static int diff_outf( DiffOutState * o, char const * fmt, ... ){
  va_list va;
  va_start(va,fmt);
  fsl_appendfv(fsl_appendf_f_diff_out, o, fmt, va);
  va_end(va);
  return o->rc;
}

/*
** Append a single line of context-diff output to pOut.
*/
static int appendDiffLine(
  DiffOutState *pOut,         /* Where to write the line of output */
  char cPrefix,       /* One of " ", "+",  or "-" */
  DLine *pLine,       /* The line to be output */
  int html,           /* True if generating HTML.  False for plain text */
  ReCompiled *pRe     /* Colorize only if line matches this Regex */
){
  int rc;
  rc = diff_out(pOut, &cPrefix, 1);
  if(rc) return rc;
  else if( html ){
    /* char *zHtml; */
    if( pRe /*MISSING: && re_dline_match(pRe, pLine, 1)==0 */ ){
      cPrefix = ' ';
    }else if( cPrefix=='+' ){
      rc = diff_out(pOut, "<span class=\"fsl-diffadd\">", -1);
    }else if( cPrefix=='-' ){
      rc = diff_out(pOut, "<span class=\"fsl-diffrm\">", -1);
    }
    if(!rc){
      rc = pOut->rc = fsl_htmlize(pOut->out, pOut->oState,
                                  pLine->z, (pLine->h & LENGTH_MASK));
      if( !rc && cPrefix!=' ' ){
        rc = diff_out(pOut, "</span>", -1);
      }
    }
  }else{
    rc = diff_out(pOut, pLine->z, pLine->h & LENGTH_MASK);
  }
  if(!rc) rc = diff_out(pOut, "\n", 1);
  return rc;
}


/*
** Add two line numbers to the beginning of an output line for a context
** diff.  One or the other of the two numbers might be zero, which means
** to leave that number field blank.  The "html" parameter means to format
** the output for HTML.
*/
static int appendDiffLineno(DiffOutState *pOut, int lnA,
                            int lnB, int html){
  int rc = 0;
  if( html ){
    rc = diff_out(pOut, "<span class=\"fsl-diffln\">", -1);
  }
  if(!rc){
    if( lnA>0 ){
      rc = diff_outf(pOut, "%6d ", lnA);
    }else{
      rc = diff_out(pOut, "       ", 7);
    }
  }
  if(!rc){
    if( lnB>0 ){
      rc = diff_outf(pOut, "%6d  ", lnB);
    }else{
      rc = diff_out(pOut, "        ", 8);
    }
    if( !rc && html ){
      rc = diff_out(pOut, "</span>", -1);
    }
  }
  return rc;
}


/*
** Minimum of two values
*/
#define minInt(a,b) (((a)<(b)) ? (a) : (b))


/**
 ** Compute the optimal longest common subsequence (LCS) using an
 ** exhaustive search.  This version of the LCS is only used for
 ** shorter input strings since runtime is O(N*N) where N is the
 ** input string length.
 */
static void optimalLCS(
  DContext *p,               /* Two files being compared */
  int iS1, int iE1,          /* Range of lines in p->aFrom[] */
  int iS2, int iE2,          /* Range of lines in p->aTo[] */
  int *piSX, int *piEX,      /* Write p->aFrom[] common segment here */
  int *piSY, int *piEY       /* Write p->aTo[] common segment here */
){
  int mxLength = 0;          /* Length of longest common subsequence */
  int i, j;                  /* Loop counters */
  int k;                     /* Length of a candidate subsequence */
  int iSXb = iS1;            /* Best match so far */
  int iSYb = iS2;            /* Best match so far */

  for(i=iS1; i<iE1-mxLength; i++){
    for(j=iS2; j<iE2-mxLength; j++){
      if( !same_dline(&p->aFrom[i], &p->aTo[j]) ) continue;
      if( mxLength && !same_dline(&p->aFrom[i+mxLength], &p->aTo[j+mxLength]) ){
        continue;
      }
      k = 1;
      while( i+k<iE1 && j+k<iE2 && same_dline(&p->aFrom[i+k],&p->aTo[j+k]) ){
        k++;
      }
      if( k>mxLength ){
        iSXb = i;
        iSYb = j;
        mxLength = k;
      }
    }
  }
  *piSX = iSXb;
  *piEX = iSXb + mxLength;
  *piSY = iSYb;
  *piEY = iSYb + mxLength;
}

/**
 ** Compare two blocks of text on lines iS1 through iE1-1 of the aFrom[]
 ** file and lines iS2 through iE2-1 of the aTo[] file.  Locate a sequence
 ** of lines in these two blocks that are exactly the same.  Return
 ** the bounds of the matching sequence.
 **
 ** If there are two or more possible answers of the same length, the
 ** returned sequence should be the one closest to the center of the
 ** input range.
 **
 ** Ideally, the common sequence should be the longest possible common
 ** sequence.  However, an exact computation of LCS is O(N*N) which is
 ** way too slow for larger files.  So this routine uses an O(N)
 ** heuristic approximation based on hashing that usually works about
 ** as well.  But if the O(N) algorithm doesn't get a good solution
 ** and N is not too large, we fall back to an exact solution by
 ** calling optimalLCS().
 */
static void longestCommonSequence(
  DContext *p,               /* Two files being compared */
  int iS1, int iE1,          /* Range of lines in p->aFrom[] */
  int iS2, int iE2,          /* Range of lines in p->aTo[] */
  int *piSX, int *piEX,      /* Write p->aFrom[] common segment here */
  int *piSY, int *piEY       /* Write p->aTo[] common segment here */
){
  int i, j, k;               /* Loop counters */
  int n;                     /* Loop limit */
  DLine *pA, *pB;            /* Pointers to lines */
  int iSX, iSY, iEX, iEY;    /* Current match */
  int skew = 0;              /* How lopsided is the match */
  int dist = 0;              /* Distance of match from center */
  int mid;                   /* Center of the span */
  int iSXb, iSYb, iEXb, iEYb;   /* Best match so far */
  int iSXp, iSYp, iEXp, iEYp;   /* Previous match */
  sqlite3_int64 bestScore;      /* Best score so far */
  sqlite3_int64 score;          /* Score for current candidate LCS */
  int span;                     /* combined width of the input sequences */

  span = (iE1 - iS1) + (iE2 - iS2);
  bestScore = -10000;
  score = 0;
  iSXb = iSXp = iS1;
  iEXb = iEXp = iS1;
  iSYb = iSYp = iS2;
  iEYb = iEYp = iS2;
  mid = (iE1 + iS1)/2;
  for(i=iS1; i<iE1; i++){
    int limit = 0;
    j = p->aTo[p->aFrom[i].h % p->nTo].iHash;
    while( j>0
      && (j-1<iS2 || j>=iE2 || !same_dline(&p->aFrom[i], &p->aTo[j-1]))
    ){
      if( limit++ > 10 ){
        j = 0;
        break;
      }
      j = p->aTo[j-1].iNext;
    }
    if( j==0 ) continue;
    assert( i>=iSXb && i>=iSXp );
    if( i<iEXb && j>=iSYb && j<iEYb ) continue;
    if( i<iEXp && j>=iSYp && j<iEYp ) continue;
    iSX = i;
    iSY = j-1;
    pA = &p->aFrom[iSX-1];
    pB = &p->aTo[iSY-1];
    n = minInt(iSX-iS1, iSY-iS2);
    for(k=0; k<n && same_dline(pA,pB); k++, pA--, pB--){}
    iSX -= k;
    iSY -= k;
    iEX = i+1;
    iEY = j;
    pA = &p->aFrom[iEX];
    pB = &p->aTo[iEY];
    n = minInt(iE1-iEX, iE2-iEY);
    for(k=0; k<n && same_dline(pA,pB); k++, pA++, pB++){}
    iEX += k;
    iEY += k;
    skew = (iSX-iS1) - (iSY-iS2);
    if( skew<0 ) skew = -skew;
    dist = (iSX+iEX)/2 - mid;
    if( dist<0 ) dist = -dist;
    score = (iEX - iSX)*(sqlite3_int64)span - (skew + dist);
    if( score>bestScore ){
      bestScore = score;
      iSXb = iSX;
      iSYb = iSY;
      iEXb = iEX;
      iEYb = iEY;
    }else if( iEX>iEXp ){
      iSXp = iSX;
      iSYp = iSY;
      iEXp = iEX;
      iEYp = iEY;
    }
  }
  if( iSXb==iEXb && (iE1-iS1)*(iE2-iS2)<400 ){
    /* If no common sequence is found using the hashing heuristic and
    ** the input is not too big, use the expensive exact solution */
    optimalLCS(p, iS1, iE1, iS2, iE2, piSX, piEX, piSY, piEY);
  }else{
    *piSX = iSXb;
    *piSY = iSYb;
    *piEX = iEXb;
    *piEY = iEYb;
  }
}

/**
 ** Expand the size of p->aEdit array to hold at least nEdit elements.
 */
static int expandEdit(DContext *p, int nEdit){
  void * re = fsl_realloc(p->aEdit, nEdit*sizeof(int));
  if(!re) return FSL_RC_OOM;
  else{
    p->aEdit = (int*)re;
    p->nEditAlloc = nEdit;
    return 0;
  }
}

/**
 ** Append a new COPY/DELETE/INSERT triple.
 **
 ** Returns 0 on success, FSL_RC_OOM on OOM.
 */
static int appendTriple(DContext *p, int nCopy, int nDel, int nIns){
  /* printf("APPEND %d/%d/%d\n", nCopy, nDel, nIns); */
  if( p->nEdit>=3 ){
    if( p->aEdit[p->nEdit-1]==0 ){
      if( p->aEdit[p->nEdit-2]==0 ){
        p->aEdit[p->nEdit-3] += nCopy;
        p->aEdit[p->nEdit-2] += nDel;
        p->aEdit[p->nEdit-1] += nIns;
        return 0;
      }
      if( nCopy==0 ){
        p->aEdit[p->nEdit-2] += nDel;
        p->aEdit[p->nEdit-1] += nIns;
        return 0;
      }
    }
    if( nCopy==0 && nDel==0 ){
      p->aEdit[p->nEdit-1] += nIns;
      return 0;
    }
  }
  if( p->nEdit+3>p->nEditAlloc ){
    int const rc = expandEdit(p, p->nEdit*2 + 15);
    if(rc) return rc;
    else if( p->aEdit==0 ) return 0;
  }
  p->aEdit[p->nEdit++] = nCopy;
  p->aEdit[p->nEdit++] = nDel;
  p->aEdit[p->nEdit++] = nIns;
  return 0;
}


/**
 ** Do a single step in the difference.  Compute a sequence of
 ** copy/delete/insert steps that will convert lines iS1 through iE1-1
 ** of the input into lines iS2 through iE2-1 of the output and write
 ** that sequence into the difference context.
 **
 ** The algorithm is to find a block of common text near the middle of
 ** the two segments being diffed.  Then recursively compute
 ** differences on the blocks before and after that common segment.
 ** Special cases apply if either input segment is empty or if the two
 ** segments have no text in common.
 */
static int diff_step(DContext *p, int iS1, int iE1, int iS2, int iE2){
  int iSX, iEX, iSY, iEY;
  int rc = 0;
  if( iE1<=iS1 ){
    /* The first segment is empty */
    if( iE2>iS2 ){
      rc = appendTriple(p, 0, 0, iE2-iS2);
    }
    return rc;
  }
  if( iE2<=iS2 ){
    /* The second segment is empty */
    return appendTriple(p, 0, iE1-iS1, 0);
  }

  /* Find the longest matching segment between the two sequences */
  longestCommonSequence(p, iS1, iE1, iS2, iE2, &iSX, &iEX, &iSY, &iEY);

  if( iEX>iSX ){
    /* A common segment has been found.
    ** Recursively diff either side of the matching segment */
    rc = diff_step(p, iS1, iSX, iS2, iSY);
    if( !rc){
      if(iEX>iSX){
        rc = appendTriple(p, iEX - iSX, 0, 0);
      }
      if(!rc) rc = diff_step(p, iEX, iE1, iEY, iE2);
    }
  }else{
    /* The two segments have nothing in common.  Delete the first then
    ** insert the second. */
    rc = appendTriple(p, 0, iE1-iS1, iE2-iS2);
  }
  return rc;
}

/**
 ** Compute the differences between two files already loaded into
 ** the DContext structure.
 **
 ** A divide and conquer technique is used.  We look for a large
 ** block of common text that is in the middle of both files.  Then
 ** compute the difference on those parts of the file before and
 ** after the common block.  This technique is fast, but it does
 ** not necessarily generate the minimum difference set.  On the
 ** other hand, we do not need a minimum difference set, only one
 ** that makes sense to human readers, which this algorithm does.
 **
 ** Any common text at the beginning and end of the two files is
 ** removed before starting the divide-and-conquer algorithm.
 **
 ** Returns 0 on succes, FSL_RC_OOM on an allocation error.
 */
static int diff_all(DContext *p){
  int mnE, iS, iE1, iE2;
  int rc = 0;
  /* Carve off the common header and footer */
  iE1 = p->nFrom;
  iE2 = p->nTo;
  while( iE1>0 && iE2>0 && same_dline(&p->aFrom[iE1-1], &p->aTo[iE2-1]) ){
    iE1--;
    iE2--;
  }
  mnE = iE1<iE2 ? iE1 : iE2;
  for(iS=0; iS<mnE && same_dline(&p->aFrom[iS],&p->aTo[iS]); iS++){}

  /* do the difference */
  if( iS>0 ){
    rc = appendTriple(p, iS, 0, 0);
    if(rc) return rc;
  }
  rc = diff_step(p, iS, iE1, iS, iE2);
  if(rc) return rc;
  if( iE1<p->nFrom ){
    rc = appendTriple(p, p->nFrom - iE1, 0, 0);
    if(rc) return rc;
  }

  /* Terminate the COPY/DELETE/INSERT triples with three zeros */
  rc = expandEdit(p, p->nEdit+3);
  if(rc) return rc;
  else if( p->aEdit ){
    p->aEdit[p->nEdit++] = 0;
    p->aEdit[p->nEdit++] = 0;
    p->aEdit[p->nEdit++] = 0;
  }
  return rc;
}


/*
** Attempt to shift insertion or deletion blocks so that they begin and
** end on lines that are pure whitespace.  In other words, try to transform
** this:
**
**      int func1(int x){
**         return x*10;
**     +}
**     +
**     +int func2(int x){
**     +   return x*20;
**      }
**
**      int func3(int x){
**         return x/5;
**      }
**
** Into one of these:
**
**      int func1(int x){              int func1(int x){
**         return x*10;                   return x*10;
**      }                              }
**     +
**     +int func2(int x){             +int func2(int x){
**     +   return x*20;               +   return x*20;
**     +}                             +}
**                                    +
**      int func3(int x){              int func3(int x){
**         return x/5;                    return x/5;
**      }                              }
*/
static void diff_optimize(DContext *p){
  int r;       /* Index of current triple */
  int lnFrom;  /* Line number in p->aFrom */
  int lnTo;    /* Line number in p->aTo */
  int cpy, del, ins;

  lnFrom = lnTo = 0;
  for(r=0; r<p->nEdit; r += 3){
    cpy = p->aEdit[r];
    del = p->aEdit[r+1];
    ins = p->aEdit[r+2];
    lnFrom += cpy;
    lnTo += cpy;

    /* Shift insertions toward the beginning of the file */
    while( cpy>0 && del==0 && ins>0 ){
      DLine *pTop = &p->aFrom[lnFrom-1];  /* Line before start of insert */
      DLine *pBtm = &p->aTo[lnTo+ins-1];  /* Last line inserted */
      if( same_dline(pTop, pBtm)==0 ) break;
      if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break;
      lnFrom--;
      lnTo--;
      p->aEdit[r]--;
      p->aEdit[r+3]++;
      cpy--;
    }

    /* Shift insertions toward the end of the file */
    while( r+3<p->nEdit && p->aEdit[r+3]>0 && del==0 && ins>0 ){
      DLine *pTop = &p->aTo[lnTo];       /* First line inserted */
      DLine *pBtm = &p->aTo[lnTo+ins];   /* First line past end of insert */
      if( same_dline(pTop, pBtm)==0 ) break;
      if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop+1)+LENGTH(pBtm) ) break;
      lnFrom++;
      lnTo++;
      p->aEdit[r]++;
      p->aEdit[r+3]--;
      cpy++;
    }

    /* Shift deletions toward the beginning of the file */
    while( cpy>0 && del>0 && ins==0 ){
      DLine *pTop = &p->aFrom[lnFrom-1];     /* Line before start of delete */
      DLine *pBtm = &p->aFrom[lnFrom+del-1]; /* Last line deleted */
      if( same_dline(pTop, pBtm)==0 ) break;
      if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break;
      lnFrom--;
      lnTo--;
      p->aEdit[r]--;
      p->aEdit[r+3]++;
      cpy--;
    }

    /* Shift deletions toward the end of the file */
    while( r+3<p->nEdit && p->aEdit[r+3]>0 && del>0 && ins==0 ){
      DLine *pTop = &p->aFrom[lnFrom];     /* First line deleted */
      DLine *pBtm = &p->aFrom[lnFrom+del]; /* First line past end of delete */
      if( same_dline(pTop, pBtm)==0 ) break;
      if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop)+LENGTH(pBtm) ) break;
      lnFrom++;
      lnTo++;
      p->aEdit[r]++;
      p->aEdit[r+3]--;
      cpy++;
    }

    lnFrom += del;
    lnTo += ins;
  }
}


/*
** Given a raw diff p[] in which the p->aEdit[] array has been filled
** in, compute a context diff into pOut.
*/
static int contextDiff(
  DContext *p,      /* The difference */
  DiffOutState *pOut,       /* Output a context diff to here */
  ReCompiled *pRe,  /* Only show changes that match this regex */
  u64 diffFlags     /* Flags controlling the diff format */
){
  DLine *A;     /* Left side of the diff */
  DLine *B;     /* Right side of the diff */
  int a = 0;    /* Index of next line in A[] */
  int b = 0;    /* Index of next line in B[] */
  int *R;       /* Array of COPY/DELETE/INSERT triples */
  int r;        /* Index into R[] */
  int nr;       /* Number of COPY/DELETE/INSERT triples to process */
  int mxr;      /* Maximum value for r */
  int na, nb;   /* Number of lines shown from A and B */
  int i, j;     /* Loop counters */
  int m;        /* Number of lines to output */
  int skip;     /* Number of lines to skip */
  static int nChunk = 0;  /* Number of diff chunks seen so far */
  int nContext;    /* Number of lines of context */
  int showLn;      /* Show line numbers */
  int html;        /* Render as HTML */
  int showDivider = 0;  /* True to show the divider between diff blocks */
  int rc = 0;
  nContext = diff_context_lines(diffFlags);
  showLn = (diffFlags & DIFF_LINENO)!=0;
  html = (diffFlags & DIFF_HTML)!=0;
  A = p->aFrom;
  B = p->aTo;
  R = p->aEdit;
  mxr = p->nEdit;
  while( mxr>2 && R[mxr-1]==0 && R[mxr-2]==0 ){ mxr -= 3; }
  for(r=0; r<mxr; r += 3*nr){
    /* Figure out how many triples to show in a single block */
    for(nr=1; R[r+nr*3]>0 && R[r+nr*3]<nContext*2; nr++){}
    /* printf("r=%d nr=%d\n", r, nr); */

    /* If there is a regex, skip this block (generate no diff output)
    ** if the regex matches or does not match both insert and delete.
    ** Only display the block if one side matches but the other side does
    ** not.
    */
#if 0
    /* MISSING: re. i would prefer a predicate function,
       anyway.
    */
    if( pRe ){
      int hideBlock = 1;
      int xa = a, xb = b;
      for(i=0; hideBlock && i<nr; i++){
        int c1, c2;
        xa += R[r+i*3];
        xb += R[r+i*3];
        c1 = re_dline_match(pRe, &A[xa], R[r+i*3+1]);
        c2 = re_dline_match(pRe, &B[xb], R[r+i*3+2]);
        hideBlock = c1==c2;
        xa += R[r+i*3+1];
        xb += R[r+i*3+2];
      }
      if( hideBlock ){
        a = xa;
        b = xb;
        continue;
      }
    }
#endif
    /* For the current block comprising nr triples, figure out
    ** how many lines of A and B are to be displayed
    */
    if( R[r]>nContext ){
      na = nb = nContext;
      skip = R[r] - nContext;
    }else{
      na = nb = R[r];
      skip = 0;
    }
    for(i=0; i<nr; i++){
      na += R[r+i*3+1];
      nb += R[r+i*3+2];
    }
    if( R[r+nr*3]>nContext ){
      na += nContext;
      nb += nContext;
    }else{
      na += R[r+nr*3];
      nb += R[r+nr*3];
    }
    for(i=1; i<nr; i++){
      na += R[r+i*3];
      nb += R[r+i*3];
    }

    /* Show the header for this block, or if we are doing a modified
    ** context diff that contains line numbers, show the separator from
    ** the previous block.
    */
    nChunk++;
    if( showLn ){
      if( !showDivider ){
        /* Do not show a top divider */
        showDivider = 1;
      }else if( html ){
        rc = diff_outf(pOut, "<span class=\"fsl-diffhr\">%.80c</span>\n", '.');
      }else{
        rc = diff_outf(pOut, "%.80c\n", '.');
      }
      if( !rc && html ){
        rc = diff_outf(pOut, "<span id=\"chunk%d\"></span>", nChunk);
      }
    }else{
      if( html ) rc = diff_outf(pOut, "<span class=\"fsl-diffln\">");
      /*
       * If the patch changes an empty file or results in an empty file,
       * the block header must use 0,0 as position indicator and not 1,0.
       * Otherwise, patch would be confused and may reject the diff.
       */
      if(!rc) rc = diff_outf(pOut,"@@ -%d,%d +%d,%d @@",
                             na ? a+skip+1 : 0, na,
                             nb ? b+skip+1 : 0, nb);
      if( !rc ){
        if( html ) rc = diff_outf(pOut, "</span>");
        if(!rc) rc = diff_out(pOut, "\n", 1);
      }
    }
    if(rc) return rc;

    /* Show the initial common area */
    a += skip;
    b += skip;
    m = R[r] - skip;
    for(j=0; !rc && j<m; j++){
      if( showLn ) rc = appendDiffLineno(pOut, a+j+1, b+j+1, html);
      if(!rc) rc = appendDiffLine(pOut, ' ', &A[a+j], html, 0);
    }
    if(rc) return rc;
    a += m;
    b += m;

    /* Show the differences */
    for(i=0; i<nr; i++){
      m = R[r+i*3+1];
      for(j=0; !rc && j<m; j++){
        if( showLn ) rc = appendDiffLineno(pOut, a+j+1, 0, html);
        if(!rc) rc = appendDiffLine(pOut, '-', &A[a+j], html, pRe);
      }
      if(rc) return rc;
      a += m;
      m = R[r+i*3+2];
      for(j=0; !rc && j<m; j++){
        if( showLn ) rc = appendDiffLineno(pOut, 0, b+j+1, html);
        if(!rc) rc = appendDiffLine(pOut, '+', &B[b+j], html, pRe);
      }
      if(rc) return rc;
      b += m;
      if( i<nr-1 ){
        m = R[r+i*3+3];
        for(j=0; !rc && j<m; j++){
          if( showLn ) rc = appendDiffLineno(pOut, a+j+1, b+j+1, html);
          if(!rc) rc = appendDiffLine(pOut, ' ', &B[b+j], html, 0);
        }
        if(rc) return rc;
        b += m;
        a += m;
      }
    }

    /* Show the final common area */
    assert( nr==i );
    m = R[r+nr*3];
    if( m>nContext ) m = nContext;
    for(j=0; !rc && j<m; j++){
      if( showLn ) rc = appendDiffLineno(pOut, a+j+1, b+j+1, html);
      if(!rc) rc = appendDiffLine(pOut, ' ', &B[b+j], html, 0);
    }
  }/*big for() loop*/
  return rc;
}

/*
** Status of a single output line
*/
typedef struct SbsLine SbsLine;
struct SbsLine {
  fsl_buffer *apCols[5];   /* Array of pointers to output columns */
  int width;               /* Maximum width of a column in the output */
  unsigned char escHtml;   /* True to escape html characters */
  int iStart;              /* Write zStart prior to character iStart */
  const char *zStart;      /* A <span> tag */
  int iEnd;                /* Write </span> prior to character iEnd */
  int iStart2;             /* Write zStart2 prior to character iStart2 */
  const char *zStart2;     /* A <span> tag */
  int iEnd2;               /* Write </span> prior to character iEnd2 */
  ReCompiled *pRe;         /* Only colorize matching lines, if not NULL */
};

/*
** Column indices for SbsLine.apCols[]
*/
#define SBS_LNA  0     /* Left line number */
#define SBS_TXTA 1     /* Left text */
#define SBS_MKR  2     /* Middle separator column */
#define SBS_LNB  3     /* Right line number */
#define SBS_TXTB 4     /* Right text */

/*
** Append newlines to all columns.
*/
static int sbsWriteNewlines(SbsLine *p){
  int i;
  int rc = 0;
  for( i=p->escHtml ? SBS_LNA : SBS_TXTB; !rc && i<=SBS_TXTB; i++ ){
    rc = fsl_buffer_append(p->apCols[i], "\n", 1);
  }
  return rc;
}

/*
** Append n spaces to the column.
*/
static int sbsWriteSpace(SbsLine *p, int n, int col){
  return fsl_buffer_appendf(p->apCols[col], "%*s", n, "");
}

/*
** Write the text of pLine into column iCol of p.
**
** If outputting HTML, write the full line.  Otherwise, only write the
** width characters.  Translate tabs into spaces.  Add newlines if col
** is SBS_TXTB.  Translate HTML characters if escHtml is true.  Pad the
** rendering to width bytes if col is SBS_TXTA and escHtml is false.
**
** This comment contains multibyte unicode characters (ü, Æ, ð) in order
** to test the ability of the diff code to handle such characters.
*/
static int sbsWriteText(SbsLine *p, DLine *pLine, int col){
  fsl_buffer *pCol = p->apCols[col];
  int rc = 0;
  int n = pLine->h & LENGTH_MASK;
  int i;   /* Number of input characters consumed */
  int k;   /* Cursor position */
  int needEndSpan = 0;
  const char *zIn = pLine->z;
  int w = p->width;
  int colorize = p->escHtml;
#if 0
  /* MISSING: re bits, but want to replace those with a predicate. */
  if( colorize && p->pRe && re_dline_match(p->pRe, pLine, 1)==0 ){
    colorize = 0;
  }
#endif
  for(i=k=0; !rc && (p->escHtml || k<w) && i<n; i++, k++){
    char c = zIn[i];
    if( colorize ){
      if( i==p->iStart ){
        int x = strlen(p->zStart);
        rc = fsl_buffer_append(pCol, p->zStart, x);
        if(rc) break;
        needEndSpan = 1;
        if( p->iStart2 ){
          p->iStart = p->iStart2;
          p->zStart = p->zStart2;
          p->iStart2 = 0;
        }
      }else if( i==p->iEnd ){
        rc = fsl_buffer_append(pCol, "</span>", 7);
        if(rc) break;
        needEndSpan = 0;
        if( p->iEnd2 ){
          p->iEnd = p->iEnd2;
          p->iEnd2 = 0;
        }
      }
    }
    if( c=='\t' && !p->escHtml ){
      rc = fsl_buffer_append(pCol, " ", 1);
      while( !rc && (k&7)!=7 && (p->escHtml || k<w) ){
        rc = fsl_buffer_append(pCol, " ", 1);
        k++;
      }
    }else if( c=='\r' || c=='\f' ){
      rc = fsl_buffer_append(pCol, " ", 1);
    }else if( c=='<' && p->escHtml ){
      rc = fsl_buffer_append(pCol, "&lt;", 4);
    }else if( c=='&' && p->escHtml ){
      rc = fsl_buffer_append(pCol, "&amp;", 5);
    }else if( c=='>' && p->escHtml ){
      rc = fsl_buffer_append(pCol, "&gt;", 4);
    }else if( c=='"' && p->escHtml ){
      rc = fsl_buffer_append(pCol, "&quot;", 6);
    }else{
      rc = fsl_buffer_append(pCol, &zIn[i], 1);
      if( (c&0xc0)==0x80 ) k--;
    }
  }
  if( !rc && needEndSpan ){
    rc = fsl_buffer_append(pCol, "</span>", 7);
  }
  if(!rc){
    if( col==SBS_TXTB ){
      rc = sbsWriteNewlines(p);
    }else if( !p->escHtml ){
      rc = sbsWriteSpace(p, w-k, SBS_TXTA);
    }
  }
  return rc;
}


/*
** Append a column to the final output blob.
*/
static int sbsWriteColumn(DiffOutState *pOut, fsl_buffer const *pCol, int col){
  return diff_outf(pOut,
    "<td><div class=\"fsl-diff%scol\">\n"
    "<pre>\n"
    "%b"
    "</pre>\n"
    "</div></td>\n",
    col % 3 ? (col == SBS_MKR ? "mkr" : "txt") : "ln",
    pCol
  );
}


/*
** Append a separator line to column iCol
*/
static int sbsWriteSep(SbsLine *p, int len, int col){
  char ch = '.';
  if( len<1 ){
    len = 1;
    ch = ' ';
  }
  return fsl_buffer_appendf(p->apCols[col],
                            "<span class=\"fsl-diffhr\">%.*c</span>\n",
                            len, ch);
}


/*
** Append the appropriate marker into the center column of the diff.
*/
static int sbsWriteMarker(SbsLine *p, const char *zTxt,
                                 const char *zHtml){
  return fsl_buffer_append(p->apCols[SBS_MKR], p->escHtml ? zHtml : zTxt, -1);
}

/*
** Append a line number to the column.
*/
static int sbsWriteLineno(SbsLine *p, int ln, int col){
  int rc;
  if( p->escHtml ){
    rc = fsl_buffer_appendf(p->apCols[col], "%d", ln+1);
  }else{
    char zLn[8];
    fsl_snprintf(zLn, 8, "%5d ", ln+1);
    rc = fsl_buffer_appendf(p->apCols[col], "%s ", zLn);
  }
  return rc;
}


/*
** The two text segments zLeft and zRight are known to be different on
** both ends, but they might have  a common segment in the middle.  If
** they do not have a common segment, return 0.  If they do have a large
** common segment, return 1 and before doing so set:
**
**   aLCS[0] = start of the common segment in zLeft
**   aLCS[1] = end of the common segment in zLeft
**   aLCS[2] = start of the common segment in zLeft
**   aLCS[3] = end of the common segment in zLeft
**
** This computation is for display purposes only and does not have to be
** optimal or exact.
*/
static int textLCS(
  const char *zLeft,  int nA,       /* String on the left */
  const char *zRight,  int nB,      /* String on the right */
  int *aLCS                         /* Identify bounds of LCS here */
){
  const unsigned char *zA = (const unsigned char*)zLeft;    /* left string */
  const unsigned char *zB = (const unsigned char*)zRight;   /* right string */
  int nt;                    /* Number of target points */
  int ti[3];                 /* Index for start of each 4-byte target */
  unsigned int target[3];    /* 4-byte alignment targets */
  unsigned int probe;        /* probe to compare against target */
  int iAS, iAE, iBS, iBE;    /* Range of common segment */
  int i, j;                  /* Loop counters */
  int rc = 0;                /* Result code.  1 for success */

  if( nA<6 || nB<6 ) return 0;
  memset(aLCS, 0, sizeof(int)*4);
  ti[0] = i = nB/2-2;
  target[0] = (zB[i]<<24) | (zB[i+1]<<16) | (zB[i+2]<<8) | zB[i+3];
  probe = 0;
  if( nB<16 ){
    nt = 1;
  }else{
    ti[1] = i = nB/4-2;
    target[1] = (zB[i]<<24) | (zB[i+1]<<16) | (zB[i+2]<<8) | zB[i+3];
    ti[2] = i = (nB*3)/4-2;
    target[2] = (zB[i]<<24) | (zB[i+1]<<16) | (zB[i+2]<<8) | zB[i+3];
    nt = 3;
  }
  probe = (zA[0]<<16) | (zA[1]<<8) | zA[2];
  for(i=3; i<nA; i++){
    probe = (probe<<8) | zA[i];
    for(j=0; j<nt; j++){
      if( probe==target[j] ){
        iAS = i-3;
        iAE = i+1;
        iBS = ti[j];
        iBE = ti[j]+4;
        while( iAE<nA && iBE<nB && zA[iAE]==zB[iBE] ){ iAE++; iBE++; }
        while( iAS>0 && iBS>0 && zA[iAS-1]==zB[iBS-1] ){ iAS--; iBS--; }
        if( iAE-iAS > aLCS[1] - aLCS[0] ){
          aLCS[0] = iAS;
          aLCS[1] = iAE;
          aLCS[2] = iBS;
          aLCS[3] = iBE;
          rc = 1;
        }
      }
    }
  }
  return rc;
}


/*
** Try to shift iStart as far as possible to the left.
*/
static void sbsShiftLeft(SbsLine *p, const char *z){
  int i, j;
  while( (i=p->iStart)>0 && z[i-1]==z[i] ){
    for(j=i+1; j<p->iEnd && z[j-1]==z[j]; j++){}
    if( j<p->iEnd ) break;
    p->iStart--;
    p->iEnd--;
  }
}

/*
** Simplify iStart and iStart2:
**
**    *  If iStart is a null-change then move iStart2 into iStart
**    *  Make sure any null-changes are in canonoical form.
**    *  Make sure all changes are at character boundaries for
**       multi-byte characters.
*/
static void sbsSimplifyLine(SbsLine *p, const char *z){
  if( p->iStart2==p->iEnd2 ){
    p->iStart2 = p->iEnd2 = 0;
  }else if( p->iStart2 ){
    while( p->iStart2>0 && (z[p->iStart2]&0xc0)==0x80 ) p->iStart2--;
    while( (z[p->iEnd2]&0xc0)==0x80 ) p->iEnd2++;
  }
  if( p->iStart==p->iEnd ){
    p->iStart = p->iStart2;
    p->iEnd = p->iEnd2;
    p->zStart = p->zStart2;
    p->iStart2 = 0;
    p->iEnd2 = 0;
  }
  if( p->iStart==p->iEnd ){
    p->iStart = p->iEnd = -1;
  }else if( p->iStart>0 ){
    while( p->iStart>0 && (z[p->iStart]&0xc0)==0x80 ) p->iStart--;
    while( (z[p->iEnd]&0xc0)==0x80 ) p->iEnd++;
  }
}


/*
** Write out lines that have been edited.  Adjust the highlight to cover
** only those parts of the line that actually changed.
*/
static int sbsWriteLineChange(
  SbsLine *p,          /* The SBS output line */
  DLine *pLeft,        /* Left line of the change */
  int lnLeft,          /* Line number for the left line */
  DLine *pRight,       /* Right line of the change */
  int lnRight          /* Line number of the right line */
){
  int rc = 0;
  int nLeft;           /* Length of left line in bytes */
  int nRight;          /* Length of right line in bytes */
  int nShort;          /* Shortest of left and right */
  int nPrefix;         /* Length of common prefix */
  int nSuffix;         /* Length of common suffix */
  const char *zLeft;   /* Text of the left line */
  const char *zRight;  /* Text of the right line */
  int nLeftDiff;       /* nLeft - nPrefix - nSuffix */
  int nRightDiff;      /* nRight - nPrefix - nSuffix */
  int aLCS[4];         /* Bounds of common middle segment */
  static const char zClassRm[]   = "<span class=\"fsl-diffrm\">";
  static const char zClassAdd[]  = "<span class=\"fsl-diffadd\">";
  static const char zClassChng[] = "<span class=\"fsl-diffchng\">";

  nLeft = pLeft->h & LENGTH_MASK;
  zLeft = pLeft->z;
  nRight = pRight->h & LENGTH_MASK;
  zRight = pRight->z;
  nShort = nLeft<nRight ? nLeft : nRight;

  nPrefix = 0;
  while( nPrefix<nShort && zLeft[nPrefix]==zRight[nPrefix] ){
    nPrefix++;
  }
  if( nPrefix<nShort ){
    while( nPrefix>0 && (zLeft[nPrefix]&0xc0)==0x80 ) nPrefix--;
  }
  nSuffix = 0;
  if( nPrefix<nShort ){
    while( nSuffix<nShort && zLeft[nLeft-nSuffix-1]==zRight[nRight-nSuffix-1] ){
      nSuffix++;
    }
    if( nSuffix<nShort ){
      while( nSuffix>0 && (zLeft[nLeft-nSuffix]&0xc0)==0x80 ) nSuffix--;
    }
    if( nSuffix==nLeft || nSuffix==nRight ) nPrefix = 0;
  }
  if( nPrefix+nSuffix > nShort ) nPrefix = nShort - nSuffix;

  /* A single chunk of text inserted on the right */
  if( nPrefix+nSuffix==nLeft ){
    rc = sbsWriteLineno(p, lnLeft, SBS_LNA);
    if(rc) return rc;
    p->iStart2 = p->iEnd2 = 0;
    p->iStart = p->iEnd = -1;
    rc = sbsWriteText(p, pLeft, SBS_TXTA);
    if( !rc && nLeft==nRight && zLeft[nLeft]==zRight[nRight] ){
      rc = sbsWriteMarker(p, "   ", "");
    }else{
      rc = sbsWriteMarker(p, " | ", "|");
    }
    if(!rc){
      rc = sbsWriteLineno(p, lnRight, SBS_LNB);
      if(!rc){
        p->iStart = nPrefix;
        p->iEnd = nRight - nSuffix;
        p->zStart = zClassAdd;
        rc = sbsWriteText(p, pRight, SBS_TXTB);
      }
    }
    return rc;
  }

  /* A single chunk of text deleted from the left */
  if( nPrefix+nSuffix==nRight ){
    /* Text deleted from the left */
    rc = sbsWriteLineno(p, lnLeft, SBS_LNA);
    if(!rc){
      p->iStart2 = p->iEnd2 = 0;
      p->iStart = nPrefix;
      p->iEnd = nLeft - nSuffix;
      p->zStart = zClassRm;
      rc = sbsWriteText(p, pLeft, SBS_TXTA);
      if(!rc){
        rc = sbsWriteMarker(p, " | ", "|");
        if(!rc){
          rc = sbsWriteLineno(p, lnRight, SBS_LNB);
          if(!rc){
            p->iStart = p->iEnd = -1;
            sbsWriteText(p, pRight, SBS_TXTB);
          }
        }
      }
    }
    return rc;
  }

  /* At this point we know that there is a chunk of text that has
  ** changed between the left and the right.  Check to see if there
  ** is a large unchanged section in the middle of that changed block.
  */
  nLeftDiff = nLeft - nSuffix - nPrefix;
  nRightDiff = nRight - nSuffix - nPrefix;
  if( p->escHtml
   && nLeftDiff >= 6
   && nRightDiff >= 6
   && textLCS(&zLeft[nPrefix], nLeftDiff, &zRight[nPrefix], nRightDiff, aLCS)
  ){
    rc = sbsWriteLineno(p, lnLeft, SBS_LNA);
    if(rc) return rc;
    p->iStart = nPrefix;
    p->iEnd = nPrefix + aLCS[0];
    if( aLCS[2]==0 ){
      sbsShiftLeft(p, pLeft->z);
      p->zStart = zClassRm;
    }else{
      p->zStart = zClassChng;
    }
    p->iStart2 = nPrefix + aLCS[1];
    p->iEnd2 = nLeft - nSuffix;
    p->zStart2 = aLCS[3]==nRightDiff ? zClassRm : zClassChng;
    sbsSimplifyLine(p, zLeft+nPrefix);
    rc = sbsWriteText(p, pLeft, SBS_TXTA);
    if(!rc) rc = sbsWriteMarker(p, " | ", "|");
    if(!rc) rc = sbsWriteLineno(p, lnRight, SBS_LNB);
    if(rc) return rc;
    p->iStart = nPrefix;
    p->iEnd = nPrefix + aLCS[2];
    if( aLCS[0]==0 ){
      sbsShiftLeft(p, pRight->z);
      p->zStart = zClassAdd;
    }else{
      p->zStart = zClassChng;
    }
    p->iStart2 = nPrefix + aLCS[3];
    p->iEnd2 = nRight - nSuffix;
    p->zStart2 = aLCS[1]==nLeftDiff ? zClassAdd : zClassChng;
    sbsSimplifyLine(p, zRight+nPrefix);
    rc = sbsWriteText(p, pRight, SBS_TXTB);
    return rc;
  }

  /* If all else fails, show a single big change between left and right */
  rc = sbsWriteLineno(p, lnLeft, SBS_LNA);
  if(!rc){
    p->iStart2 = p->iEnd2 = 0;
    p->iStart = nPrefix;
    p->iEnd = nLeft - nSuffix;
    p->zStart = zClassChng;
    rc = sbsWriteText(p, pLeft, SBS_TXTA);
    if(!rc){
      rc = sbsWriteMarker(p, " | ", "|");
      if(!rc){
        rc = sbsWriteLineno(p, lnRight, SBS_LNB);
        if(!rc){
          p->iEnd = nRight - nSuffix;
          sbsWriteText(p, pRight, SBS_TXTB);
        }
      }
    }
  }
  return rc;
}


/*
** Return the number between 0 and 100 that is smaller the closer pA and
** pB match.  Return 0 for a perfect match.  Return 100 if pA and pB are
** completely different.
**
** The current algorithm is as follows:
**
** (1) Remove leading and trailing whitespace.
** (2) Truncate both strings to at most 250 characters
** (3) Find the length of the longest common subsequence
** (4) Longer common subsequences yield lower scores.
*/
static int match_dline(DLine *pA, DLine *pB){
  const char *zA;            /* Left string */
  const char *zB;            /* right string */
  int nA;                    /* Bytes in zA[] */
  int nB;                    /* Bytes in zB[] */
  int avg;                   /* Average length of A and B */
  int i, j, k;               /* Loop counters */
  int best = 0;              /* Longest match found so far */
  int score;                 /* Final score.  0..100 */
  unsigned char c;           /* Character being examined */
  unsigned char aFirst[256]; /* aFirst[X] = index in zB[] of first char X */
  unsigned char aNext[252];  /* aNext[i] = index in zB[] of next zB[i] char */

  zA = pA->z;
  zB = pB->z;
  nA = pA->h & LENGTH_MASK;
  nB = pB->h & LENGTH_MASK;
  while( nA>0 && fsl_isspace(zA[0]) ){ nA--; zA++; }
  while( nA>0 && fsl_isspace(zA[nA-1]) ){ nA--; }
  while( nB>0 && fsl_isspace(zB[0]) ){ nB--; zB++; }
  while( nB>0 && fsl_isspace(zB[nB-1]) ){ nB--; }
  if( nA>250 ) nA = 250;
  if( nB>250 ) nB = 250;
  avg = (nA+nB)/2;
  if( avg==0 ) return 0;
  if( nA==nB && memcmp(zA, zB, nA)==0 ) return 0;
  memset(aFirst, 0, sizeof(aFirst));
  zA--; zB--;   /* Make both zA[] and zB[] 1-indexed */
  for(i=nB; i>0; i--){
    c = (unsigned char)zB[i];
    aNext[i] = aFirst[c];
    aFirst[c] = i;
  }
  best = 0;
  for(i=1; i<=nA-best; i++){
    c = (unsigned char)zA[i];
    for(j=aFirst[c]; j>0 && j<nB-best; j = aNext[j]){
      int limit = minInt(nA-i, nB-j);
      for(k=1; k<=limit && zA[k+i]==zB[k+j]; k++){}
      if( k>best ) best = k;
    }
  }
  score = (best>avg) ? 0 : (avg - best)*100/avg;

#if 0
  fprintf(stderr, "A: [%.*s]\nB: [%.*s]\nbest=%d avg=%d score=%d\n",
  nA, zA+1, nB, zB+1, best, avg, score);
#endif

  /* Return the result */
  return score;
}


/*
** There is a change block in which nLeft lines of text on the left are
** converted into nRight lines of text on the right.  This routine computes
** how the lines on the left line up with the lines on the right.
**
** The return value is a buffer of unsigned characters, obtained from
** fsl_malloc().  (The caller needs to free the return value using
** fsl_free().)  Entries in the returned array have values as follows:
**
**    1.  Delete the next line of pLeft.
**    2.  Insert the next line of pRight.
**    3.  The next line of pLeft changes into the next line of pRight.
**    4.  Delete one line from pLeft and add one line to pRight.
**
** Values larger than three indicate better matches.
**
** The length of the returned array will be just large enough to cause
** all elements of pLeft and pRight to be consumed.
**
** Algorithm:  Wagner's minimum edit-distance algorithm, modified by
** adding a cost to each match based on how well the two rows match
** each other.  Insertion and deletion costs are 50.  Match costs
** are between 0 and 100 where 0 is a perfect match 100 is a complete
** mismatch.
*/
static unsigned char *sbsAlignment(
   DLine *aLeft, int nLeft,       /* Text on the left */
   DLine *aRight, int nRight      /* Text on the right */
){
  int i, j, k;                 /* Loop counters */
  int *a;                      /* One row of the Wagner matrix */
  int *pToFree;                /* Space that needs to be freed */
  unsigned char *aM;           /* Wagner result matrix */
  int nMatch, iMatch;          /* Number of matching lines and match score */
  int mnLen;                   /* MIN(nLeft, nRight) */
  int mxLen;                   /* MAX(nLeft, nRight) */
  int aBuf[100];               /* Stack space for a[] if nRight not to big */

  aM = (unsigned char *)fsl_malloc( (nLeft+1)*(nRight+1) );
  if(!aM) return NULL;
  if( nLeft==0 ){
    memset(aM, 2, nRight);
    return aM;
  }
  if( nRight==0 ){
    memset(aM, 1, nLeft);
    return aM;
  }

  /* This algorithm is O(N**2).  So if N is too big, bail out with a
  ** simple (but stupid and ugly) result that doesn't take too long. */
  mnLen = nLeft<nRight ? nLeft : nRight;
  if( nLeft*nRight>100000 ){
    memset(aM, 4, mnLen);
    if( nLeft>mnLen )  memset(aM+mnLen, 1, nLeft-mnLen);
    if( nRight>mnLen ) memset(aM+mnLen, 2, nRight-mnLen);
    return aM;
  }

  if( nRight < (sizeof(aBuf)/sizeof(aBuf[0]))-1 ){
    pToFree = 0;
    a = aBuf;
  }else{
    a = pToFree = fsl_malloc( sizeof(a[0])*(nRight+1) );
    if(!a){
      fsl_free(aM);
      return NULL;
    }
  }

  /* Compute the best alignment */
  for(i=0; i<=nRight; i++){
    aM[i] = 2;
    a[i] = i*50;
  }
  aM[0] = 0;
  for(j=1; j<=nLeft; j++){
    int p = a[0];
    a[0] = p+50;
    aM[j*(nRight+1)] = 1;
    for(i=1; i<=nRight; i++){
      int m = a[i-1]+50;
      int d = 2;
      if( m>a[i]+50 ){
        m = a[i]+50;
        d = 1;
      }
      if( m>p ){
        int score = match_dline(&aLeft[j-1], &aRight[i-1]);
        if( (score<=63 || (i<j+1 && i>j-1)) && m>p+score ){
          m = p+score;
          d = 3 | score*4;
        }
      }
      p = a[i];
      a[i] = m;
      aM[j*(nRight+1)+i] = d;
    }
  }

  /* Compute the lowest-cost path back through the matrix */
  i = nRight;
  j = nLeft;
  k = (nRight+1)*(nLeft+1)-1;
  nMatch = iMatch = 0;
  while( i+j>0 ){
    unsigned char c = aM[k];
    if( c>=3 ){
      assert( i>0 && j>0 );
      i--;
      j--;
      nMatch++;
      iMatch += (c>>2);
      aM[k] = 3;
    }else if( c==2 ){
      assert( i>0 );
      i--;
    }else{
      assert( j>0 );
      j--;
    }
    k--;
    aM[k] = aM[j*(nRight+1)+i];
  }
  k++;
  i = (nRight+1)*(nLeft+1) - k;
  memmove(aM, &aM[k], i);

  /* If:
  **   (1) the alignment is more than 25% longer than the longest side, and
  **   (2) the average match cost exceeds 15
  ** Then this is probably an alignment that will be difficult for humans
  ** to read.  So instead, just show all of the right side inserted followed
  ** by all of the left side deleted.
  **
  ** The coefficients for conditions (1) and (2) above are determined by
  ** experimentation.
  */
  mxLen = nLeft>nRight ? nLeft : nRight;
  if( i*4>mxLen*5 && (nMatch==0 || iMatch/nMatch>15) ){
    memset(aM, 4, mnLen);
    if( nLeft>mnLen )  memset(aM+mnLen, 1, nLeft-mnLen);
    if( nRight>mnLen ) memset(aM+mnLen, 2, nRight-mnLen);
  }

  /* Return the result */
  fsl_free(pToFree);
  return aM;
}

/*
** R[] is an array of six integer, two COPY/DELETE/INSERT triples for a
** pair of adjacent differences.  Return true if the gap between these
** two differences is so small that they should be rendered as a single
** edit.
*/
static int smallGap(int *R){
  return R[3]<=2 || R[3]<=(R[1]+R[2]+R[4]+R[5])/8;
}

/*
** Given a diff context in which the aEdit[] array has been filled
** in, compute a side-by-side diff into pOut.
*/
static int sbsDiff(
  DContext *p,       /* The computed diff */
  DiffOutState *pOut,        /* Write the results here */
  ReCompiled *pRe,   /* Only show changes that match this regex */
  u64 diffFlags      /* Flags controlling the diff */
){
  DLine *A;     /* Left side of the diff */
  DLine *B;     /* Right side of the diff */
  int rc = 0;
  int a = 0;    /* Index of next line in A[] */
  int b = 0;    /* Index of next line in B[] */
  int *R;       /* Array of COPY/DELETE/INSERT triples */
  int r;        /* Index into R[] */
  int nr;       /* Number of COPY/DELETE/INSERT triples to process */
  int mxr;      /* Maximum value for r */
  int na, nb;   /* Number of lines shown from A and B */
  int i, j;     /* Loop counters */
  int m, ma, mb;/* Number of lines to output */
  int skip;     /* Number of lines to skip */
  static int nChunk = 0; /* Number of chunks of diff output seen so far */
  SbsLine s;    /* Output line buffer */
  int nContext; /* Lines of context above and below each change */
  int showDivider = 0;  /* True to show the divider */
  fsl_buffer unesc = fsl_buffer_empty;
  fsl_buffer aCols[5] = { /* Array of column blobs */
  fsl_buffer_empty_m, fsl_buffer_empty_m, fsl_buffer_empty_m,
  fsl_buffer_empty_m, fsl_buffer_empty_m
  }; 

  memset(&s, 0, sizeof(s));
  s.width = diff_width(diffFlags);
  nContext = diff_context_lines(diffFlags);
  s.escHtml = (diffFlags & DIFF_HTML)!=0;
  if( s.escHtml ){
    for(i=SBS_LNA; i<=SBS_TXTB; i++){
      s.apCols[i] = &aCols[i];
    }
  }else{
    for(i=SBS_LNA; i<=SBS_TXTB; i++){
      s.apCols[i] = &unesc;
    }
  }
  s.pRe = pRe;
  s.iStart = -1;
  s.iStart2 = 0;
  s.iEnd = -1;
  A = p->aFrom;
  B = p->aTo;
  R = p->aEdit;
  mxr = p->nEdit;
  while( mxr>2 && R[mxr-1]==0 && R[mxr-2]==0 ){ mxr -= 3; }

  for(r=0; r<mxr; r += 3*nr){
    /* Figure out how many triples to show in a single block */
    for(nr=1; R[r+nr*3]>0 && R[r+nr*3]<nContext*2; nr++){}
    /* printf("r=%d nr=%d\n", r, nr); */
#if 0
    /* MISSING: re/predicate bits. */
    /* If there is a regex, skip this block (generate no diff output)
    ** if the regex matches or does not match both insert and delete.
    ** Only display the block if one side matches but the other side does
    ** not.
    */
    if( pRe ){
      int hideBlock = 1;
      int xa = a, xb = b;
      for(i=0; hideBlock && i<nr; i++){
        int c1, c2;
        xa += R[r+i*3];
        xb += R[r+i*3];
        c1 = re_dline_match(pRe, &A[xa], R[r+i*3+1]);
        c2 = re_dline_match(pRe, &B[xb], R[r+i*3+2]);
        hideBlock = c1==c2;
        xa += R[r+i*3+1];
        xb += R[r+i*3+2];
      }
      if( hideBlock ){
        a = xa;
        b = xb;
        continue;
      }
    }
#endif
    /* For the current block comprising nr triples, figure out
    ** how many lines of A and B are to be displayed
    */
    if( R[r]>nContext ){
      na = nb = nContext;
      skip = R[r] - nContext;
    }else{
      na = nb = R[r];
      skip = 0;
    }
    for(i=0; i<nr; i++){
      na += R[r+i*3+1];
      nb += R[r+i*3+2];
    }
    if( R[r+nr*3]>nContext ){
      na += nContext;
      nb += nContext;
    }else{
      na += R[r+nr*3];
      nb += R[r+nr*3];
    }
    for(i=1; i<nr; i++){
      na += R[r+i*3];
      nb += R[r+i*3];
    }

    /* Draw the separator between blocks */
    if( showDivider ){
      if( s.escHtml ){
        char zLn[10];
        fsl_snprintf(zLn, sizeof(zLn), "%d", a+skip+1);
        rc = sbsWriteSep(&s, strlen(zLn), SBS_LNA);
        if(!rc) rc = sbsWriteSep(&s, s.width, SBS_TXTA);
        if(!rc) rc = sbsWriteSep(&s, 0, SBS_MKR);
        if(!rc){
          fsl_snprintf(zLn, sizeof(zLn), "%d", b+skip+1);
          rc = sbsWriteSep(&s, strlen(zLn), SBS_LNB);
          if(!rc) rc = sbsWriteSep(&s, s.width, SBS_TXTB);
        }
      }else{
        diff_outf(pOut, "%.*c\n", s.width*2+16, '.');
      }
      if(rc) goto end;
    }
    showDivider = 1;
    nChunk++;
    if( s.escHtml ){
      rc = fsl_buffer_appendf(s.apCols[SBS_LNA],
                              "<span id=\"chunk%d\"></span>",
                              nChunk);
    }

    /* Show the initial common area */
    a += skip;
    b += skip;
    m = R[r] - skip;
    for(j=0; !rc && j<m; j++){
      rc = sbsWriteLineno(&s, a+j, SBS_LNA);
      if(rc) break;
      s.iStart = s.iEnd = -1;
      rc = sbsWriteText(&s, &A[a+j], SBS_TXTA);
      if(!rc) rc = sbsWriteMarker(&s, "   ", "");
      if(!rc) rc = sbsWriteLineno(&s, b+j, SBS_LNB);
      if(!rc) rc = sbsWriteText(&s, &B[b+j], SBS_TXTB);
    }
    if(rc) goto end;
    a += m;
    b += m;

    /* Show the differences */
    for(i=0; i<nr; i++){
      unsigned char *alignment;
      ma = R[r+i*3+1];   /* Lines on left but not on right */
      mb = R[r+i*3+2];   /* Lines on right but not on left */

      /* If the gap between the current diff and then next diff within the
      ** same block is not too great, then render them as if they are a
      ** single diff. */
      while( i<nr-1 && smallGap(&R[r+i*3]) ){
        i++;
        m = R[r+i*3];
        ma += R[r+i*3+1] + m;
        mb += R[r+i*3+2] + m;
      }

      alignment = sbsAlignment(&A[a], ma, &B[b], mb);
      if(!alignment){
        rc = FSL_RC_OOM;
        goto end;
      }
      for(j=0; !rc && ma+mb>0; j++){
        if( alignment[j]==1 ){
          /* Delete one line from the left */
          rc = sbsWriteLineno(&s, a, SBS_LNA);
          if(rc) goto end_align;
          s.iStart = 0;
          s.zStart = "<span class=\"fsl-diffrm\">";
          s.iEnd = LENGTH(&A[a]);
          rc = sbsWriteText(&s, &A[a], SBS_TXTA);
          if(!rc) rc = sbsWriteMarker(&s, " <", "&lt;");
          if(!rc) rc = sbsWriteNewlines(&s);
          if(rc) goto end_align;
          assert( ma>0 );
          ma--;
          a++;
        }else if( alignment[j]==3 ){
          /* The left line is changed into the right line */
          rc = sbsWriteLineChange(&s, &A[a], a, &B[b], b);
          if(rc) goto end_align;
          assert( ma>0 && mb>0 );
          ma--;
          mb--;
          a++;
          b++;
        }else if( alignment[j]==2 ){
          /* Insert one line on the right */
          if( !s.escHtml ){
            rc = sbsWriteSpace(&s, s.width + 7, SBS_TXTA);
          }
          if(!rc) rc = sbsWriteMarker(&s, " > ", "&gt;");
          if(!rc) rc = sbsWriteLineno(&s, b, SBS_LNB);
          if(rc) goto end_align;
          s.iStart = 0;
          s.zStart = "<span class=\"fsl-diffadd\">";
          s.iEnd = LENGTH(&B[b]);
          rc = sbsWriteText(&s, &B[b], SBS_TXTB);
          if(rc) goto end_align;
          assert( mb>0 );
          mb--;
          b++;
        }else{
          /* Delete from the left and insert on the right */
          rc = sbsWriteLineno(&s, a, SBS_LNA);
          if(rc) goto end_align;
          s.iStart = 0;
          s.zStart = "<span class=\"fsl-diffrm\">";
          s.iEnd = LENGTH(&A[a]);
          rc = sbsWriteText(&s, &A[a], SBS_TXTA);
          if(!rc) rc = sbsWriteMarker(&s, " | ", "|");
          if(!rc) rc = sbsWriteLineno(&s, b, SBS_LNB);
          if(rc) goto end_align;
          s.iStart = 0;
          s.zStart = "<span class=\"fsl-diffadd\">";
          s.iEnd = LENGTH(&B[b]);
          rc = sbsWriteText(&s, &B[b], SBS_TXTB);
          if(rc) goto end_align;
          ma--;
          mb--;
          a++;
          b++;
        }
      }
      end_align:
      fsl_free(alignment);
      if(rc) goto end;
      if( i<nr-1 ){
        m = R[r+i*3+3];
        for(j=0; !rc && j<m; j++){
          rc = sbsWriteLineno(&s, a+j, SBS_LNA);
          s.iStart = s.iEnd = -1;
          if(!rc) rc = sbsWriteText(&s, &A[a+j], SBS_TXTA);
          if(!rc) rc = sbsWriteMarker(&s, "   ", "");
          if(!rc) rc = sbsWriteLineno(&s, b+j, SBS_LNB);
          if(!rc) rc = sbsWriteText(&s, &B[b+j], SBS_TXTB);
        }
        if(rc) goto end;
        b += m;
        a += m;
      }
    }

    /* Show the final common area */
    assert( nr==i );
    m = R[r+nr*3];
    if( m>nContext ) m = nContext;
    for(j=0; !rc && j<m; j++){
      rc = sbsWriteLineno(&s, a+j, SBS_LNA);
      s.iStart = s.iEnd = -1;
      if(!rc) rc = sbsWriteText(&s, &A[a+j], SBS_TXTA);
      if(!rc) rc = sbsWriteMarker(&s, "   ", "");
      if(!rc) rc = sbsWriteLineno(&s, b+j, SBS_LNB);
      if(!rc) rc = sbsWriteText(&s, &B[b+j], SBS_TXTB);
    }
    if(rc) goto end;
  } /* diff triplet loop */
  assert(!rc);
  
  if( s.escHtml && (s.apCols[SBS_LNA]->used>0) ){
    rc = diff_out(pOut, "<table class=\"fsl-sbsdiffcols\"><tr>\n", -1);
    for(i=SBS_LNA; !rc && i<=SBS_TXTB; i++){
      rc = sbsWriteColumn(pOut, s.apCols[i], i);
    }
    rc = diff_out(pOut, "</tr></table>\n", -1);
  }else if(unesc.used){
    rc = pOut->out(pOut->oState, unesc.mem, unesc.used);
  }

  end:
  for( i = 0; i < sizeof(aCols)/sizeof(aCols[0]); ++i ){
    fsl_buffer_clear(&aCols[i]);
  }
  fsl_buffer_clear(&unesc);
  return rc;
}



static int fsl_diff_text_impl(
  fsl_buffer const *pA,   /* FROM file */
  fsl_buffer const *pB,   /* TO file */
  fsl_output_f out, void * outState,
  /* ReCompiled *pRe, */ /* Only output changes where this Regexp matches */
  short contextLines,
  short sbsWidth,
  int diffFlags_,    /* FSL_DIFF_* flags */
  int ** outRaw
){
  int ignoreEolWs; /* Ignore whitespace at the end of lines */
  int rc;
  DContext c;
  DiffOutState dos = DiffOutState_empty;
  fsl_uint64_t diffFlags = fsl_diff_flags_convert(diffFlags_);
  if(!pA || !pB || (!out && !outRaw)) return FSL_RC_MISUSE;
  else if(contextLines<=0) contextLines = 5;
  else if(contextLines & ~LENGTH_MASK) contextLines = (int)LENGTH_MASK;
  diffFlags |= (LENGTH_MASK & contextLines);
  /* Encode SBS width... */
  if(sbsWidth<0
     || ((DIFF_SIDEBYSIDE & diffFlags) && !sbsWidth) ) sbsWidth = 80;
  if(sbsWidth) diffFlags |= DIFF_SIDEBYSIDE;
  diffFlags |= ((int)(sbsWidth & 0xFF))<<16;
  dos.out = out;
  dos.oState = outState;
  
  if( diffFlags & DIFF_INVERT ){
    fsl_buffer const *pTemp = pA;
    pA = pB;
    pB = pTemp;
  }
  ignoreEolWs = (diffFlags & DIFF_IGNORE_EOLWS)!=0;

  /* Prepare the input files */
  memset(&c, 0, sizeof(c));
  rc = break_into_lines(fsl_buffer_cstr(pA), fsl_buffer_size(pA),
                        &c.nFrom, &c.aFrom, ignoreEolWs);
  if(rc) goto end;
  rc = break_into_lines(fsl_buffer_cstr(pB), fsl_buffer_size(pB),
                        &c.nTo, &c.aTo, ignoreEolWs);
  if(rc) goto end;

  if( c.aFrom==0 || c.aTo==0 ){
    /* Empty diff */
    goto end;
  }

  /* Compute the difference */
  rc = diff_all(&c);
  if(rc) goto end;
  if( (diffFlags & DIFF_NOTTOOBIG)!=0 ){
    int i, m, n;
    int *a = c.aEdit;
    int mx = c.nEdit;
    for(i=m=n=0; i<mx; i+=3){ m += a[i]; n += a[i+1]+a[i+2]; }
    if( n>10000 ){
      rc = FSL_RC_RANGE;
      /* diff_errmsg(pOut, DIFF_TOO_MANY_CHANGES, diffFlags); */
      goto end;;
    }
  }
  if( (diffFlags & DIFF_NOOPT)==0 ){
    diff_optimize(&c);
  }

  if( out ){
    /* Compute a context or side-by-side diff into pOut */
    /* MISSING: */
    if( diffFlags & DIFF_SIDEBYSIDE ){
      rc = sbsDiff(&c, &dos, NULL/*pRe*/, diffFlags);
    }else{
      rc = contextDiff(&c, &dos, NULL/*pRe*/, diffFlags);
    }
  }else if(outRaw){
    /* If a context diff is not requested, then return the
    ** array of COPY/DELETE/INSERT triples.
    */
    *outRaw = c.aEdit;
    c.aEdit = NULL;
  }
  end:
  fsl_free(c.aFrom);
  fsl_free(c.aTo);
  fsl_free(c.aEdit);
  return rc;
}

int fsl_diff_text(fsl_buffer const *pA, fsl_buffer const *pB,
                  fsl_output_f out, void * outState,
                  short contextLines,
                  short sbsWidth,
                  int diffFlags ){
  return fsl_diff_text_impl(pA, pB, out, outState,
                            contextLines, sbsWidth, diffFlags, NULL );
}



int fsl_diff_text_to_buffer(fsl_buffer const *pA, fsl_buffer const *pB,
                            fsl_buffer * pOut,
                            short contextLines,
                            short sbsWidth,
                            int diffFlags ){
  return (pA && pB && pOut)
    ? fsl_diff_text_impl(pA, pB, fsl_output_f_buffer, pOut,
                         contextLines, sbsWidth, diffFlags, NULL )
    : FSL_RC_MISUSE;
}

#undef TO_BE_STATIC
#undef LENGTH_MASK
#undef LENGTH_MASK_SZ
#undef LENGTH
#undef DIFF_CONTEXT_MASK
#undef DIFF_WIDTH_MASK
#undef DIFF_IGNORE_EOLWS
#undef DIFF_SIDEBYSIDE
#undef DIFF_VERBOSE
#undef DIFF_BRIEF
#undef DIFF_INLINE
#undef DIFF_HTML
#undef DIFF_LINENO
#undef DIFF_WS_WARNING
#undef DIFF_NOOPT
#undef DIFF_INVERT
#undef DIFF_CONTEXT_EX
#undef DIFF_NOTTOOBIG
#undef minInt