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 guillemets, ceux "corrects" ont une ouverture et une fermeture , il faut donc la remplacer 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é» est au tout début ou à la fin d'une phrase ou d'une ligne.

Exemples

Entrées possibles (attention, français dedans! :)):
⋅ 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!"
⋅ Les messages "Inc" ou "rect" quo "ne doivent" pas être remplacés.
⋅ J'ai dit: "Si ça marche aussi avec les 'singles', j'adorerais 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!"
⋅ Les messages "Inc" ou "rect" quo "ne doivent" pas être remplacés.
⋅ J'ai dit: "Si ça marche aussi sur les" singles ", j'adorerais encore plus!"

Sorties incorrectes:
⋅ Le panneau indique: "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 signe de fermeture.
→ Il doit y avoir un espace entre un guillemet de fermeture et un mot.
→ Il doit y avoir un espace entre un mot et un guillemet d'ouverture.
→ Il ne doit pas y avoir d'espace entre un guillemet d'ouverture et sa citation.

Le besoin

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

Notez que je n'utilise pas (ne peux pas) le délimiteur de mot "\ b" dans une expression régulière 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 dresser une liste de ce que je considère comme un délimiteur de mots et l'utiliser dans une expression régulière. Mais je préfère avoir une belle fonction de travail plutôt qu'une liste!

Toute idée sera la bienvenue.

15
Takit Isy 13 avril 2018 à 14:35

3 réponses

Meilleure réponse

J'ai obtenu une solution qui correspond enfin à tous mes besoins.
J'avoue que c'est beaucoup plus compliqué que celui de T.J., ce qui peut être parfait pour les cas simples.

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

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

… Et j'ai eu beaucoup de mal à jouer beaucoup avec RegEx pour finalement arriver à 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 sur 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>

Il semble fonctionner avec tout ce que je peux imaginer en ce 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 les guillemets doubles et simples.

Quoi qu'il en soit, comme je ne suis pas du tout un expert 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 22 mai 2018 à 09:52

Il fonctionne pour de nombreux cas, à l'exception du cas où le "mot" est 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'une assertion de début / fin de ligne et de l'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 à la 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 que vous vouliez éviter d’échapper des caractères lors de la saisie des utilisateurs. Je ne sais pas où vous prévoyez de l'utiliser, mais quelque chose comme ce qui précède n'est presque jamais l'approche à utiliser pour un problème avec ce genre de description.

4
T.J. Crowder 13 avril 2018 à 12:02

Donc, au lieu de suivre une approche de remplacement d'expression régulière, j'utiliserais une boucle simple avec un acte d'équilibrage des citations. Vous supposez que chaque citation qui apparaît correspondra à une autre et quand elle le fera, elle sera remplacée par paires.

Voici 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}'`)
} ;
}

Mise à jour 1: 20 avril 2018

J'ai mis à jour la fonction. Il peut toujours y avoir des cas marginaux, mais vous devez tout mettre dans le test et l'exécuter et corriger ceux qui ne se comportent pas comme prévu

0
Tarun Lalwani 20 avril 2018 à 04:49