web-dev-qa-db-fra.com

Remplacer les guillemets doubles par des guillemets

Je cherche un moyen de remplacer les guillemets par des guillemets «corrigés» dans une entrée utilisateur.

L'idée

Voici un extrait montrant brièvement le principe:
Pour les devis, les “corrects” ont un d'ouverture et un de fermeture, il doit donc être remplacé dans le bon sens.

$('#myInput').on("keyup", function(e) {
  // The below doesn't work when there's no space before or after.
  this.value = this.value.replace(/ "/g, ' “');
  this.value = this.value.replace(/" /g, '” ');
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea id="myInput"></textarea>

Mais ce qui précède ne fonctionne pas dans tous les cas.
Par exemple, lorsque le "mot cité" se trouve au tout début ou à la fin d'une phrase ou d'une ligne. 

Exemples

Entrées possibles (attention, français à l'intérieur! :)):
⋅ Je suis "heureux"! Ça y est, j'ai "osé", et mon "âme sœur" était au rendez-vous…
⋅ Le panneau indique: "Du texte" du texte "du texte". et "Notez l'espace ici!"
⋅ "Inc" ou "rect", "tes" devraient "ne pas être remplacés.
J'ai dit: "Si ça marche aussi pour les célibataires, j'adorerais ça encore plus!"

Sorties correctes:
⋅ Je suis «heureux»! Ça y est, j'ai “osé”, et mon “âme sœur” était au rendez-vous…
⋅ Le panneau indique: “Du texte“ du texte ”du texte.” Et “Notez l'espace ici!”
⋅ “Inc” ou “rect” quo "devraient être" ne devraient pas être remplacés.
J’ai dit: «Si ça marche aussi pour les célibataires, j’adorerais encore plus!

Sorties incorrectes:
⋅ Le signe dit: “Du texte” du texte “du texte.” Et […]
Pourquoi c'est incorrect:
→ Il ne doit pas y avoir d'espace entre la fin d'une citation et son point de fermeture.
→ Il devrait y avoir un espace entre un guillemet et un mot.
→ Il devrait y avoir un espace entre un mot et un guillemet ouvrant.
→ Il ne doit y avoir aucun espace entre un guillemet ouvrant et sa citation. 

Le besoin

Comment pourrait-il être possible de remplacer efficacement et facilement les citations dans tous ces cas?
Si possible, j'aimerais également que la solution puisse "corriger" les guillemets même si nous les ajoutons après le dactylographie de la phrase entière.

Notez que je n’utilise pas (ne peux pas) utiliser le délimiteur Word "\ b" dans une regex car les "caractères accentués, tels que" é "ou" ü "sont, malheureusement, traités comme des sauts de mots." (Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions )

Bien sûr, s'il n'y a pas d'autre solution, je vais créer une liste de ce que je considère comme un délimiteur Word et l'utiliser dans une expression régulière. Mais je préférerais avoir une fonction de travail agréable plutôt qu'une liste!

N'importe quelle idée est la bienvenue.

15
Takit Isy

J'ai une solution qui répond finalement à tous mes besoins.
J'admets que c'est beaucoup plus compliqué que celui de T.J., qui peut être parfait pour des cas simples.

Rappelez-vous, mon problème principal était l'impossibilité d'utiliser \b à cause des caractères accentués.
J'ai pu résoudre ce problème en utilisant la solution de ce sujet:
Supprimer les accents/signes diacritiques dans une chaîne en JavaScript

Après cela, j’ai utilisé une fonction modifiée très inspirée de la réponse donnée ici…
Comment remplacer un caractère à un index particulier en JavaScript?

… Et j'ai eu beaucoup de mal à jouer avec RegEx pour arriver finalement à cette solution:

var str_orig = `· I'm "happy" ! Ça y est, j'ai "osé", et mon "âme sœur" était au rendez-vous…
· The sign says: "Some text "some text" some text." and "Note the space here !"
⋅ "Inc"or"rect" quo"tes should " not be replaced.
· I said: "If it works on 'singles' too, I'd love it even more!"
Word1" Word2"
Word1 Word2"
"Word1 Word2
"Word1" Word2
"Word1" Word2"
"Word1 Word2"`;

// Thanks, exactly what I needed!
var str_norm = str_orig.normalize('NFD').replace(/[\u0300-\u036f]/g, '');

// Thanks for inspiration
String.prototype.replaceQuoteAt = function(index, shift) {
  const replacers = "“‘”’";
  var offset = 1 * (this[index] == "'") + 2 * (shift);
  return this.substr(0, index) + replacers[offset] + this.substr(index + 1);
}

// Opening quote: not after a boundary, not before a space or at the end
var re_start = /(?!\b)["'](?!(\s|$))/gi;
while ((match = re_start.exec(str_norm)) != null) {
  str_orig = str_orig.replaceQuoteAt(match.index, false);
}

// Closing quote: not at the beginning or after a space, not before a boundary
var re_end = /(?<!(^|\s))["'](?!\b)/gi;
while ((match = re_end.exec(str_norm)) != null) {
  str_orig = str_orig.replaceQuoteAt(match.index, true);
}

console.log("Corrected: \n", str_orig);

Et ci-dessous est un extrait d'un exemple de travail avec un textarea.
Je viens de créer une fonction du code du premier extrait et j'utilise une sous-chaîne autour de la position du curseur pour filtrer l'appel de la fonction (cela évite de l'appeler à chaque entrée de caractère): 

String.prototype.replaceQuoteAt = function(index, offset) {
  const replacers = "“‘”’";
  var i = 2 * (offset) + 1 * (this[index] == "'");
  return this.substr(0, index) + replacers[i] + this.substr(index + 1);
}

function replaceQuotes(str) {
  var str_norm = str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  var re_quote_start = /(?!\b)["'](?!(\s|$))/gi;
  while ((match = re_quote_start.exec(str_norm)) != null) {
    str = str.replaceQuoteAt(match.index, false);
  }
  var re_quote_end = /(?<!(^|\s))["'](?!\b)./gi;
  while ((match = re_quote_end.exec(str_norm)) != null) {
    str = str.replaceQuoteAt(match.index, true);
  }
  return str;
}

var pasted = 0;
document.getElementById("myInput").onpaste = function(e) {
  pasted = 1;
}

document.getElementById("myInput").oninput = function(e) {
  var caretPos = this.selectionStart; // Gets caret position
  var chars = this.value.substring(caretPos - 2, caretPos + 1); // Gets 2 chars before caret (just typed and the one before), and 1 char just after
  if (pasted || chars.includes(`"`) || chars.includes(`'`)) { // Filters the calling of the function
    this.value = replaceQuotes(this.value); // Calls the function
    if (pasted) {
      pasted = 0;
    } else {
      this.setSelectionRange(caretPos, caretPos); // Restores caret position
    }
  }
}
#myInput {
  width: 90%;
  height: 100px;
}
<textarea id="myInput"></textarea>

Cela semble fonctionner avec tout ce que je peux imaginer pour le moment.
La fonction remplace correctement les guillemets lorsque:
⋅ en tapant régulièrement,
⋅ ajouter des guillemets après avoir tapé le texte,
⋅ coller du texte. 

Il remplace à la fois les citations double et simple.

Quoi qu'il en soit, comme je ne suis pas du tout un expert de RegEx, n'hésitez pas à commenter si vous remarquez un comportement qui peut être indésirable, ou un moyen d'améliorer les expressions.

1
Takit Isy

Cela fonctionne dans de nombreux cas, à l'exception du moment où le "mot" se trouve au tout début ou à la fin d'une phrase ou d'une ligne.

Pour résoudre ce problème, vous pouvez utiliser une alternance d'assertion de début/fin de ligne et d'espace, capturer cela et l'utiliser dans le remplacement:

this.value = this.value.replace(/(^| )"/g, '$1“');
this.value = this.value.replace(/"($| )/g, '”$1');

L'alternance est ^|/$|. Le groupe de capture sera "" s'il correspond à l'assertion ou " " s'il correspond à sapce.

$('#myInput').on("keyup", function(e) {
  this.value = this.value.replace(/'/g, '’');
  // The below doesn't work when there's no space before or after.
  this.value = this.value.replace(/(^| )"/g, '$1“');
  this.value = this.value.replace(/"($| )/g, '”$1');
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea id="myInput"></textarea>

Cependant , vous avez dit vouloir éviter les "caractères d'échappement" lors de la saisie de l'utilisateur. Je ne sais pas trop où vous envisagez de l'utiliser, mais quelque chose comme ce qui précède n'est presque jamais l'approche à utiliser pour résoudre un problème avec ce type de description.

4
T.J. Crowder

Ainsi, au lieu de suivre une approche de remplacement de la regex, j’utiliserais une simple boucle avec équilibrage des guillemets. Vous supposez que chaque citation apparaissant correspondra à une autre et, le cas échéant, sera remplacée par paires. 

Ci-dessous une implémentation de test pour le même

String.prototype.replaceAt=function(index, replacement) {
return this.substr(0, index) + replacement+ this.substr(index + replacement.length);
}

tests  =[
// [`I'm "happy"! J'ai enfin "osé". La rencontre de mon "âme-sœur" a "été" au rendez-vous…
// and how it should look after correction:`, `I'm "happy"! J'ai enfin "osé". La rencontre de mon "âme-sœur" a "été" au rendez-vous…
// and how it should look after correction:`],
[`tarun" lalwani"`, `tarun” lalwani”`],
[`tarun lalwani"`, `tarun lalwani”`],
[`"tarun lalwani`,`“tarun lalwani`],
[`"tarun" lalwani`,`“tarun” lalwani`],
[`"tarun" lalwani"`,`“tarun” lalwani”`],
[`"tarun lalwani"`, `“tarun lalwani”`]
]

function isCharacterSeparator(value) {
return /“, /.test(value)
}

for ([data, output] of tests) {
let qt = "“”"
let qtL = '“'
let qtR = '”'
let bal = 0
let pattern = /["“”]/g
let data_new = data
while (match = pattern.exec(data)) {
    if (bal == 0) {
        if (match.index == 0) {
            data_new = data_new.replaceAt(match.index, qt[bal]);
            bal = 1
        } else {
            if (isCharacterSeparator(data_new[match.index-1])) {
                data_new = data_new.replaceAt(match.index, qtL);
            } else {
                data_new = data_new.replaceAt(match.index, qtR);
            }
        }
    } else {
        if (match.index == data.length - 1) {
            data_new = data_new.replaceAt(match.index, qtR);
        } else if (isCharacterSeparator(data_new[match.index-1])) {
            if (isCharacterSeparator(data_new[match.index+1])) {
                //previous is separator as well as next one too
                // "tarun " lalwani"
                // take a call what needs to be done here?

            } else {
                data_new = data_new.replaceAt(match.index, qtL);
            }
        } else {
            if (isCharacterSeparator(data_new[match.index+1])) {
                data_new = data_new.replaceAt(match.index, qtL);
            } else {
                data_new = data_new.replaceAt(match.index, qtR);
            }
        }
    }


}

console.log(data_new)
if (data_new != output) {
  console.log(`Failed to parse '${data}' Actual='${data_new}' Expected='${output}'`)
} ;
}

Update-1: 20-Apr-2018

J'ai mis à jour la fonction. Il peut encore y avoir des cas Edge, mais vous devriez tout mettre dans le test, le lancer et corriger ceux qui ne se comportent pas comme prévu.

0
Tarun Lalwani