Comment ça fonctionne?
Cela fonctionne en lisant un morceau de chaîne par morceau, ce qui n'est peut-être pas la meilleure solution pour les très longues chaînes.
Chaque fois que l'analyseur détecte qu'un morceau critique est en cours de lecture, c'est-à-dire '*'
ou toute autre balise de démarque, il commence à analyser des morceaux de cet élément jusqu'à ce que l'analyseur trouve sa balise de fermeture.
Il fonctionne sur des chaînes multi-lignes, voir le code par exemple.
Avertissements
Vous n'avez pas précisé, ou j'aurais pu mal comprendre vos besoins, s'il est nécessaire d'analyser les balises à la fois en gras et en italique , ma solution actuelle pourrait ne pas fonctionner dans ce cas.
Si vous avez cependant besoin de travailler avec les conditions ci-dessus, commentez ici et je modifierai le code.
Première mise à jour: modifie la façon dont les balises de démarque sont traitées
Les balises ne sont plus codées en dur, mais plutôt une carte que vous pouvez facilement étendre pour répondre à vos besoins.
Correction des bugs que vous avez mentionnés dans les commentaires, merci d'avoir signalé ce problème = p
Deuxième mise à jour: balises de démarque multi-longueur
Le moyen le plus simple d'y parvenir: remplacer les caractères multi-longueurs par un unicode rarement utilisé
Bien que la méthode parseMarkdown
ne prenne pas encore en charge les balises multi-longueur, nous pouvons facilement remplacer ces balises multi-longueur par un simple string.replace
lors de l'envoi de notre rawMarkdown
accessoire.
Pour voir un exemple de cela dans la pratique, regardez le ReactDOM.render
, situé à la fin du code.
Même si votre application prend en charge plusieurs langues, il existe toujours des caractères unicode non valides que JavaScript détecte, par exemple: "\uFFFF"
n'est pas un unicode valide, si je me souviens bien, mais JS sera toujours en mesure de le comparer ( "\uFFFF" === "\uFFFF" = true
)
Cela peut sembler piraté au début, mais, selon votre cas d'utilisation, je ne vois aucun problème majeur en utilisant cette route.
Une autre façon d'y parvenir
Eh bien, nous pourrions facilement suivre les derniers morceaux N
(où N
correspond à la longueur de la balise multi-longueur la plus longue).
Il y aurait quelques ajustements à apporter au comportement de la méthode de boucle à l'intérieur
parseMarkdown
, c'est-à-dire vérifier si le morceau actuel fait partie d'une balise de plusieurs longueurs, s'il l'utilise comme balise; sinon, dans des cas comme ``k
, nous aurions besoin de le marquer comme notMultiLength
ou quelque chose de similaire et de pousser ce morceau comme contenu.
Code
// Instead of creating hardcoded variables, we can make the code more extendable
// by storing all the possible tags we'll work with in a Map. Thus, creating
// more tags will not require additional logic in our code.
const tags = new Map(Object.entries({
"*": "strong", // bold
"!": "button", // action
"_": "em", // emphasis
"\uFFFF": "pre", // Just use a very unlikely to happen unicode character,
// We'll replace our multi-length symbols with that one.
}));
// Might be useful if we need to discover the symbol of a tag
const tagSymbols = new Map();
tags.forEach((v, k) => { tagSymbols.set(v, k ); })
const rawMarkdown = `
This must be *bold*,
This also must be *bo_ld*,
this _entire block must be
emphasized even if it's comprised of multiple lines_,
This is an !action! it should be a button,
\`\`\`
beep, boop, this is code
\`\`\`
This is an asterisk\\*
`;
class App extends React.Component {
parseMarkdown(source) {
let currentTag = "";
let currentContent = "";
const parsedMarkdown = [];
// We create this variable to track possible escape characters, eg. "\"
let before = "";
const pushContent = (
content,
tagValue,
props,
) => {
let children = undefined;
// There's the need to parse for empty lines
if (content.indexOf("\n\n") >= 0) {
let before = "";
const contentJSX = [];
let chunk = "";
for (let i = 0; i < content.length; i++) {
if (i !== 0) before = content[i - 1];
chunk += content[i];
if (before === "\n" && content[i] === "\n") {
contentJSX.push(chunk);
contentJSX.push(<br />);
chunk = "";
}
if (chunk !== "" && i === content.length - 1) {
contentJSX.push(chunk);
}
}
children = contentJSX;
} else {
children = [content];
}
parsedMarkdown.push(React.createElement(tagValue, props, children))
};
for (let i = 0; i < source.length; i++) {
const chunk = source[i];
if (i !== 0) {
before = source[i - 1];
}
// Does our current chunk needs to be treated as a escaped char?
const escaped = before === "\\";
// Detect if we need to start/finish parsing our tags
// We are not parsing anything, however, that could change at current
// chunk
if (currentTag === "" && escaped === false) {
// If our tags array has the chunk, this means a markdown tag has
// just been found. We'll change our current state to reflect this.
if (tags.has(chunk)) {
currentTag = tags.get(chunk);
// We have simple content to push
if (currentContent !== "") {
pushContent(currentContent, "span");
}
currentContent = "";
}
} else if (currentTag !== "" && escaped === false) {
// We'll look if we can finish parsing our tag
if (tags.has(chunk)) {
const symbolValue = tags.get(chunk);
// Just because the current chunk is a symbol it doesn't mean we
// can already finish our currentTag.
//
// We'll need to see if the symbol's value corresponds to the
// value of our currentTag. In case it does, we'll finish parsing it.
if (symbolValue === currentTag) {
pushContent(
currentContent,
currentTag,
undefined, // you could pass props here
);
currentTag = "";
currentContent = "";
}
}
}
// Increment our currentContent
//
// Ideally, we don't want our rendered markdown to contain any '\'
// or undesired '*' or '_' or '!'.
//
// Users can still escape '*', '_', '!' by prefixing them with '\'
if (tags.has(chunk) === false || escaped) {
if (chunk !== "\\" || escaped) {
currentContent += chunk;
}
}
// In case an erroneous, i.e. unfinished tag, is present and the we've
// reached the end of our source (rawMarkdown), we want to make sure
// all our currentContent is pushed as a simple string
if (currentContent !== "" && i === source.length - 1) {
pushContent(
currentContent,
"span",
undefined,
);
}
}
return parsedMarkdown;
}
render() {
return (
<div className="App">
<div>{this.parseMarkdown(this.props.rawMarkdown)}</div>
</div>
);
}
}
ReactDOM.render(<App rawMarkdown={rawMarkdown.replace(/```/g, "\uFFFF")} />, document.getElementById('app'));
Lien vers le code (TypeScript) https://codepen.io/ludanin/pen/GRgNWPv
Lien vers le code (vanilla / babel) https://codepen.io/ludanin/pen/eYmBvXw
font _italic *and bold* then only italic_ and normal
? Quel serait le résultat attendu? Ou ne sera-t-il jamais imbriqué?