/** * @file * Cosmetic fix: converts straight quotes to typographic (curly) quotes * in rendered field markup, on page load / AJAX render. * * IMPORTANT: This is a display-only patch. It does NOT change the stored * field data in the database — it only rewrites text nodes in the DOM * after render. Treat it as a stopgap until the migrated content itself * is corrected; remove this once that's done. * * Adjust SELECTOR below to match whichever field wrappers actually need * this (e.g. body fields, specific content types). Keep it as narrow as * you reasonably can — this walks every text node inside each match. */ (function (Drupal, once) { "use strict"; // CHANGE THIS to target only the fields that need it. const SELECTOR = ".field--name-field-history-note"; // Tags whose text content should never be touched. const SKIP_TAGS = new Set([ "SCRIPT", "STYLE", "TEXTAREA", "INPUT", "CODE", "PRE", ]); /** * Converts straight quotes/apostrophes in a string to curly equivalents. * * Heuristic (same logic SmartyPants-style tools use): * - A straight double quote at the start of the string, or preceded by * whitespace / an opening bracket / a dash, is an OPENING quote. * Everything else is a CLOSING quote. * - A straight single quote in the same "opening" positions becomes an * opening single quote (‘); every other straight single quote — * including apostrophes in contractions like don't — becomes the * closing/apostrophe glyph (’), which is correct either way. */ function smartenQuotes(text) { text = text.replace(/(^|[\s([{\-\u2014])"/g, "$1\u201C"); // opening “ text = text.replace(/"/g, "\u201D"); // remaining → ” text = text.replace(/(^|[\s([{\-\u2014])'/g, "$1\u2018"); // opening ‘ text = text.replace(/'/g, "\u2019"); // remaining → ’ (also apostrophes) return text; } function fixTextNodesIn(root) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { if (SKIP_TAGS.has(node.parentNode.nodeName)) { return NodeFilter.FILTER_REJECT; } if (!/["']/.test(node.nodeValue)) { return NodeFilter.FILTER_SKIP; } return NodeFilter.FILTER_ACCEPT; }, }); const nodes = []; let n; while ((n = walker.nextNode())) { nodes.push(n); } nodes.forEach(function (node) { node.nodeValue = smartenQuotes(node.nodeValue); }); } Drupal.behaviors.smartQuotesCosmetic = { attach: function (context) { once("smart-quotes-cosmetic", SELECTOR, context).forEach(function (el) { fixTextNodesIn(el); }); }, }; })(Drupal, once);