Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// smartQuotes.js
// by Werson
// version 1.2, 2008-05-14
// works correctly in:
// · Firefox 2.0
// works somewhat in:
// · Safari 3.1 (doesn't skip PRE/TT/CODE sections)
// 
// I, Werson, the copyright holder of this work, hereby release it into the public domain. This applies worldwide.
// In case this is not legally possible, I grant any entity the right to use this work for any purpose,
// without any conditions, unless such conditions are required by law.

// take a string and replace all straight marks with curly marks,
//   applying some basic rules (it's almost always a closing mark,
//   so figure out the few cases where it's an opening mark)
function quoteReplace(data) {

  // single quotation marks,
  //   problematic because apostrophes (same codepoint as closing quotes,
  //   unfortunately) can come at the beginning of words, so we need to
  //   list likely examples of that
  data = data.replace(/'([Nn])(\W)/g, '’$1$2');    // Guns 'n' Roses, or Guns 'n Roses, should use apostrophes
  data = data.replace(/'(\d)0s/g, '’$10s');  // '80s, '90s, etc.
  data = data.replace(/'([Tt]is)/g, '’$1');  // 'Tis the season, etc.
  data = data.replace(/'([Tt]was)/g, '’$1');  // 'Twas the season, etc.
  data = data.replace(/^'s(\W)/g, '’s$1');   // [[John]]'s (links are separate nodes,
                                             //   so this would otherwise be seen as 'S...')

  // the rest of these are homologous to double quotation marks
  data = data.replace(/\s'/g, ' ‘');
  data = data.replace(/^'([\d\w])/g, '‘$1');
  data = data.replace(/(\W)"([\d\w])/g, '$1“$2');
  data = data.replace(/'/g, '’');

  // double quotation marks
  data = data.replace(/\s"/g, ' “');              // a closing mark will never be preceded by a space
  data = data.replace(/"([\d\w])/g, '“$1');      // a closing mark will never be at the beginning of a line
                                                    //   (and won't be after a link if it's followed by a letter)
  data = data.replace(/(\W)"([\d\w])/g, '$1“$2');          // a closing mark won't be after a punctuation mark
                                                   //  but before a letter
  data = data.replace(/"/g, '”');            // anything else is probably a closer

  return data;
}

// go through document and get all text nodes, then replace quotation marks
function smartQuotes() {
  if (document.URL.indexOf('action=history') < 1 && document.URL.indexOf('action=edit') < 1) {
    // title is a special case, as replacing the node text doesn't do anything
    document.title = quoteReplace(document.title);
    // use an XPATH to find all text nodes, and run each one through quoteReplace()
    //   but do not alter PRE, CODE, or TT elements, or any of their descendents
    //   (I don't know how to do this elegantly in XPath)
    var xPathResult = document.evaluate('.//*[name(.) != "PRE" and name(.) != "CODE" and name(.) != "TT"]/*[name(.) != "PRE" and name(.) != "CODE" and name(.) != "TT"]/*[name(.) != "PRE" and name(.) != "CODE" and name(.) != "TT"]/text()[normalize-space(.) != ""]', document.documentElement, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var i = 0, l = xPathResult.snapshotLength; i < l; i++) {
      var textNode = xPathResult.snapshotItem(i);
      textNode.data = quoteReplace(textNode.data);
    }
    // crappy way to do this
    var html = document.body.innerHTML;
    document.body.innerHTML = html.replace('”<a ', '“<a ');
  }
}

// initiate smartQuotes script
$(smartQuotes);