Squish-unsquish ligatures Here is a list of some common
ligatures in Unicode (the
ones I could create with my Compose key on Debian):

Orig  Ascii  Lig
ae    [ae]   æ
AE    [AE]   Æ
oe    [oe]   œ
OE    [OE]   Œ
ij    [ij]   ĳ
IJ    [IJ]   Ĳ
ff    [ff]   ﬀ
fi    [fi]   ﬁ
fl    [fl]   ﬂ
ffi   [ffi]  ﬃ
ffl   [ffl]  ﬄ


You have two options in this challenge: use the actual UTF-8 ligatures, or use
the ASCII-only variant. If you use the actual UTF-8 ligature variants, you gain
a 20% bonus. If you use the ASCII-only variant, you may assume square brackets
will never be involved except to signify a ligature.

The challenge: given a string as input, output the same string


with all original ligatures replaced by their expanded counterparts.


match greedily: affib becomes aﬃb (a[ffi]b), not aﬀib (a[ff]ib)
or afﬁb (af[fi]b).

with all "expanded" letter sequences replaced by ligatures.


for example, æOEfoo ([ae]OEfoo) becomes aeŒfoo (ae[OE]foo).



Do this completely independently: ﬀi ([ff]i) becomes ffi (ffi), not ﬃ
([ffi]).

Sound simple enough? There's a catch: every time two non-ligatures overlap by
exactly one character, both of the ligatures must be inserted into the
string. Here's a few test cases to demonstrate:

Input   Ascii-output      Output
fij     [fi][ij]          ﬁĳ
fIJ     f[IJ]             fĲ     * remember, capitalization matters!
fffi    [ff][ffi]         ﬀﬃ
fff     [ff][ff]          ﬀﬀ
ffffi   [ff][ff][ffi]     ﬀﬀﬃ
ffffij  [ff][ff][ffi][ij] ﬀﬀﬃĳ


Be careful: the same greedy matching applies (note especially the last few test
cases).

code-golf, so shortest code in bytes wins.
JavaScript (ES6), 213 bytes - 20% bonus = 170.4

s=&gt;eval('for(p=o="";m=s.match(r="ﬄ|ﬃ|ﬂ|ﬁ|ﬀ|Ĳ|ĳ|Œ|œ|Æ|æ|ffl|ffi|fl|fi|ff|IJ|ij|OE|oe|AE|ae",x=r.split`|`);s=s.slice(i+t.length-(p=t&lt;"z")))o+=s.slice(p,i=m.index)+x[(x.indexOf(t=m[0])+11)%22];o+s.slice(p)')


Explanation

s=&gt;                           // s = input string
  eval(`                      // use eval to avoid writing {} or return
    for(                      // iterate over each ligature match
      p=                      // p = 1 if the last match was a non-unicode ligature
        o="";                 // o = output string
      m=s.match(              // find the next ligature

        // r = regex string for ligatures (unicode and non-unicode)
        r="ﬄ|ﬃ|ﬂ|ﬁ|ﬀ|Ĳ|ĳ|Œ|œ|Æ|æ|ffl|ffi|fl|fi|ff|IJ|ij|OE|oe|AE|ae",
        x=r.split\`|\`        // x = arrray of r

      );
      s=s.slice(i+t.length    // remove the part that has been added to the output
        -(p=t&lt;"z"))           // if we matched a non-unicode ligature, keep the last
    )                         //     character so it can be part of the next match
      o+=s.slice(p,i=m.index) // add the text before the match to the output
        +x[(x.indexOf(        // add the opposite type of the matched ligature
          t=m[0]              // t = matched text
        )+11)%22];            // (index + 11) % 22 returns the opposite index
    o+s.slice(p)              // return o + any remaining characters
  `)


Test



var solution = s=&gt;eval('for(p=o="";m=s.match(r="ﬄ|ﬃ|ﬂ|ﬁ|ﬀ|Ĳ|ĳ|Œ|œ|Æ|æ|ffl|ffi|fl|fi|ff|IJ|ij|OE|oe|AE|ae",x=r.split`|`);s=s.slice(i+t.length-(p=t&lt;"z")))o+=s.slice(p,i=m.index)+x[(x.indexOf(t=m[0])+11)%22];o+s.slice(p)')
&lt;input type="text" id="input" value="ﬀiffffij" oninput="result.textContent=solution(input.value)" /&gt;
&lt;pre id="result"&gt;&lt;/pre&gt;