Error: Invalid regular expression - „lookbehind assertion“

Ein ES9-Feature legte meinen Blog lahm

12.06.2023

dejavascriptnextjshowtoES9SentryRegEx
Gregor Wedlich
Gregor Wedlich
Life, the Universe and Everything.
Donate with:
Lightning
Alby

Inhaltsverzeichnis

    So schnell kann es gehen! Gerade habe ich meinen Blog online gebracht, in dem festen Glauben, dass hier alles funktioniert, da erhalte ich auch schon die erste Nachricht von einem Freund. Er wies mich darauf hin, dass ihm der Inhalt der Blogbeiträge nicht angezeigt wird. Ich wusste, dass dieser Freund seine Macs/iPhone-Geräte kaum aktualisiert, daher dachte ich mir, solange nicht jeder zweite mit diesem Problem konfrontiert wird, ignoriere ich es erst einmal. Da ich ausschließlich aktuelle Browser nutze und wir hier NextJS mit Client Side Rendering (CSR) verwenden (wodurch Benutzerfehler nicht protokolliert werden), habe ich vorsichtshalber Sentry in das Frontend eingebaut, um informiert zu werden, wenn jemand einen Fehler erhält.

    Was ist Sentry?

    Ich werde sicher bald ein eigenes Tutorial zu Sentry schreiben, bis dahin hier eine kurze Erklärung: Sentry ist ein Open-Source-Tool zur Fehlerverfolgung, das Entwicklern dabei hilft, Probleme in ihrer Software zu erkennen und zu beheben. Es überwacht und protokolliert Fehler in Echtzeit, sodass Entwickler*innen sie analysieren und beheben können, um die Leistung ihrer Anwendungen zu verbessern.

    Wie lautet der Fehler?

    Es dauerte nicht lange, bis ich weitere Meldungen von anderen Nutzern erhielt, die über das gleiche Verhalten informierten. Auch das Sentry-Log war gut gefüllt mit immer dem gleichen Fehler:

    1React ErrorBoundary Error: Invalid regular expression: invalid group specifier name

    sentry Invalid regular expression

    Nach etwas Recherche konnte ich den Fehler eingrenzen. Es war ein Problem mit dem ECMAScript 2018 (ES9) Feature „Lookbehind assertion“. Dazu später mehr.

    Ich konnte es kaum glauben, laut caniuse.com unterstützen im Jahr 2023 nur 83.97 % der Browser „Lookbehind assertion“. Hauptsächlich betrifft das natürlich fast ausschließlich ziemlich alte Browser und bei Sentry waren es meistens ältere iOS-Geräte.

    Was ist: Lookbehind assertion

    Ich wage hier mal den Versuch, „Lookbehind assertion“ zu erklären. Ich gehe davon aus, dass du weißt, was reguläre Ausdrücke sind, wenn nicht, kannst du das hier nachlesen.

    "Lookbehind Assertion" ist ein Konzept in regulären Ausdrücken (RegEx), das es ermöglicht, Übereinstimmungen basierend auf dem Kontext zu finden, der einem bestimmten Muster vorausgeht.

    Eine Lookbehind Assertion erweitert RegEx, indem es ermöglicht, Muster zu finden, die von einem bestimmten Kontext vorausgegangen sind. Zum Beispiel würde der reguläre Ausdruck /(?<=@)username/ den String "username" finden, aber nur, wenn er von einem "@"-Zeichen vorausgegangen ist.

    Es gibt zwei Arten von Lookbehind Assertions:

    • Positive Lookbehind: „(?<=Y)X“ findet "X", aber nur, wenn "Y" direkt davor steht.
    • Negative Lookbehind: „(?<!Y)X“ findet "X", aber nur, wenn "Y" nicht direkt davor steht.

    Einsatz von "Lookbehind Assertion"

    Auf sil3ntrunning.net war es so das ich meine Beiträge im Markdown Format schreibe und damit ich meinen Bildern die Deminsionen direkt mitgeben kann habe ich diese in folgenden Format dem Alt Attribute mitgegeben:

    1![MEIN-ALT-TAG {{ w: 1000, h: 500 }}](/uploads/PAPH-TO-IMG.png)

    Mit "{{ w: 1000, h: 1000 }}" gebe ich meinem Bild über den Markdown-Editor die Größe mit. Ich habe eine Komponente, welche mein Markdown rendert und dort wird nach diesen Werten gefiltert. Dazu habe ich das RegEx-Feature "Lookbehind Assertions" genutzt.

    1img: function ({ ...props }) { 2 const substrings = props.alt?.split('{{'); 3 const alt = substrings[0].trim(); 4 5 const width = substrings[1] 6 ? substrings[1].match(/(?<=w:\s?)\d+/g)[0] 7 : 800; 8 const height = substrings[1] 9 ? substrings[1].match(/(?<=h:\s?)\d+/g)[0] 10 : 400; 11 12 return ( 13 <Image src={props.src} alt={alt} width={width} height={height} /> 14 ); 15},

    Ich verwende in meinem Code also eine "positive Lookbehind Assertion": "/(?<=w:\s?)\d+/g".

    (?<=w:\s?) ist die Lookbehind Assertion. Sie sagt: "Suche nach einem Muster, das von 'w: ' vorausgegangen wird". Das \s? bedeutet, dass ein Leerzeichen optional ist. Daher wird dieses Muster sowohl auf "w:1000" als auch auf "w: 1000" passen.

    \d+ ist das eigentliche Muster, nach dem gesucht wird. Es steht für eine oder mehrere Ziffern. In meinem Fall repräsentiert es die Breite des Bildes, welche ich im Alt-Attribut angebe.

    Das g am Ende bedeutet, dass der reguläre Ausdruck global ist, d.h. er sucht nach allen Übereinstimmungen im gesamten String und nicht nur nach der ersten Übereinstimmung.

    In meinem Beispiel würde die Sequenz also die Breite "1000" finden. Genau das Gleiche passiert natürlich auch für die Höhe.

    Die Lösung des Problems

    Da aber anscheinend einige Browser dieses Feature nicht unterstützen und doch recht viele Nutzer ältere Geräte wie z.B. ein iPhone XS mit veraltetem Safari-Browser nutzen, habe ich mich daran gesetzt und meinen Code umgeschrieben.

    1img: function ({ ...props }) { 2 const substrings = props.alt?.split('{{'); 3 const alt = substrings[0].trim(); 4 5 const width = substrings[1] 6 ? substrings[1].match(/w:\s?(\d+)/)[1] 7 : 800; 8 const height = substrings[1] 9 ? substrings[1].match(/h:\s?(\d+)/)[1] 10 : 400; 11 12 return ( 13 <Image src={props.src} alt={alt} width={width} height={height} /> 14 ); 15},

    Was mein RegEx nun macht:

    w:\s? sucht nach dem Muster "w: " mit einem optionalen Leerzeichen.

    (\d+) ist eine Gruppe, die eine oder mehrere Ziffern darstellt. Die Klammern erzeugen eine "erfassende Gruppe", was bedeutet, dass der Teil des Strings, der mit diesem Muster übereinstimmt, separat von den anderen Teilen des Matches gespeichert und später abgerufen werden kann.

    Das [1] am Ende des .match()-Aufrufs holt den Wert, der mit der ersten (und einzigen) Gruppe im regulären Ausdruck übereinstimmt.

    Der Unterschied zu meinem ursprünglichen Code besteht darin, dass dieser Code nicht nach Zahlen sucht, die von "w: " bzw. "h: " vorausgegangen werden (was eine Lookbehind Assertion wäre), sondern stattdessen nach der gesamten Sequenz "w: " gefolgt von Zahlen bzw. "h: " gefolgt von Zahlen sucht und dann nur den Zahlenanteil extrahiert.

    Was ist besser?

    Diese Frage lässt sich nicht so einfach beantworten. Im Hinblick auf die Browser-Kompatibilität würde ich empfehlen, auf die Variante zu setzen, die von den meisten JavaScript-Engines unterstützt wird.

    Die Lesbarkeit von Lookbehind Assertions ist nicht gerade die einfachste. Auf der anderen Seite können Lookbehind Assertions den Code in einigen Fällen sauberer und einfacher gestalten, indem sie es ermöglichen, genauer zu definieren, wonach man sucht.

    Letztendlich musst du selbst entscheiden, was du einsetzen möchtest. Ich habe mich letztendlich für die Browser-Kompatibilität entschieden.

    Quellen:

    Image: Midjourney

    1/imagine prompt A Computer screen with an error message in a dark room, watercolored pencil, comic, vivid, vibrant --ar 16:9

    Comments: