update
This commit is contained in:
@@ -37,10 +37,17 @@ _INFINITIV = re.compile(r"(?:e[lr]?n|ln|rn)$", re.I)
|
||||
# Eine Handlung steht als Infinitiv am Anfang oder am Ende der Zeile („Bestätigen im
|
||||
# Modal", „Text ändern") oder beginnt mit einer Taste. Alles andere ist ein Ergebnis.
|
||||
# Nur Anfang und Ende, nicht irgendwo: deutsche Mehrzahl endet auch auf -en.
|
||||
_TASTE = re.compile(r"^(?:LMB|RMB|MMB|Klick|Doppelklick|Enter|Escape|Strg|Tab)\b", re.I)
|
||||
# „Befehl" und „Kommando" stehen dabei wie eine Taste: auf der Kommandozeile ist der
|
||||
# Aufruf die Handlung, und ohne sie wäre ein Setup nicht bedienbar zu beschreiben.
|
||||
_TASTE = re.compile(
|
||||
r"^(?:LMB|RMB|MMB|Klick|Doppelklick|Enter|Escape|Strg|Tab|Befehl|Kommando)\b", re.I)
|
||||
|
||||
SATZENDE = ".;"
|
||||
_KLAMMER = re.compile(r"[()/]")
|
||||
# Ein Zitat ist eine Einheit: ein Menüeintrag oder ein Kommando. „Befehl "make install"
|
||||
# ausführen" sind drei Wörter, nicht vier, und der Schrägstrich in „npm run build/dev"
|
||||
# ist keiner von uns. Darum wird außerhalb der Zitate gemessen.
|
||||
_ZITAT = re.compile(r'"[^"]*"|„[^“]*“')
|
||||
# Eine Vorlage ist kein Inhalt. Das Modell schlug `<testschritt>` als Testschritt
|
||||
# vor — formal richtig, gemeint war „hier fehlt noch etwas".
|
||||
_LUECKE = re.compile(r"<[^<>]*>|\.\.\.|\bTODO\b|\bXXX\b", re.I)
|
||||
@@ -58,9 +65,30 @@ def _problem(feld: str, original: str, problem: str, vorschlag: str = "") -> dic
|
||||
return {"feld": feld, "original": original, "problem": problem, "vorschlag": vorschlag}
|
||||
|
||||
|
||||
def _messbar(zeile: str) -> str:
|
||||
"""Die Zeile mit jedem Zitat als einem Wort — so wird gezählt und gesucht."""
|
||||
return _ZITAT.sub("Zitat", zeile)
|
||||
|
||||
|
||||
def _ausserhalb(zeile: str, wandel) -> str:
|
||||
"""Wendet `wandel` nur auf die Teile außerhalb der Zitate an.
|
||||
|
||||
Ein Füllwort im Menüeintrag ist keins — dort steht der Text, den die Anwendung
|
||||
zeigt. Würde er mitgestrichen, verwiese der Testschritt auf einen Knopf, den es
|
||||
nicht gibt.
|
||||
"""
|
||||
teile, letzt = [], 0
|
||||
for m in _ZITAT.finditer(zeile):
|
||||
teile.append(wandel(zeile[letzt:m.start()]))
|
||||
teile.append(m.group(0))
|
||||
letzt = m.end()
|
||||
teile.append(wandel(zeile[letzt:]))
|
||||
return "".join(teile)
|
||||
|
||||
|
||||
def _ohne_wort(zeile: str, wort: str) -> str:
|
||||
neu = re.sub(rf"\s*\b{re.escape(wort)}\b\.?", "", zeile, flags=re.I).strip()
|
||||
return re.sub(r"\s{2,}", " ", neu)
|
||||
weg = lambda t: re.sub(rf"\s*\b{re.escape(wort)}\b\.?", "", t, flags=re.I)
|
||||
return re.sub(r"\s{2,}", " ", _ausserhalb(zeile, weg)).strip()
|
||||
|
||||
|
||||
def _reparierbar(zeile: str) -> tuple[list[str], str]:
|
||||
@@ -70,7 +98,7 @@ def _reparierbar(zeile: str) -> tuple[list[str], str]:
|
||||
gruende.append("Satzzeichen am Zeilenende")
|
||||
neu = neu[:-1].rstrip()
|
||||
for wort in FUELLWOERTER:
|
||||
if re.search(rf"\b{wort}\b", neu, re.I):
|
||||
if re.search(rf"\b{wort}\b", _messbar(neu), re.I):
|
||||
gruende.append(f"Füllwort „{wort}“")
|
||||
neu = _ohne_wort(neu, wort)
|
||||
return gruende, neu
|
||||
@@ -79,19 +107,24 @@ def _reparierbar(zeile: str) -> tuple[list[str], str]:
|
||||
def _umschreiben(bereich: str, feld: str, zeile: str) -> list[str]:
|
||||
"""Verstöße, die der Code sieht, aber nicht beheben kann."""
|
||||
gruende = []
|
||||
if _KLAMMER.search(zeile):
|
||||
gemessen = _messbar(zeile)
|
||||
if _KLAMMER.search(gemessen):
|
||||
gruende.append("Klammer oder Schrägstrich")
|
||||
if _LUECKE.search(zeile):
|
||||
if _LUECKE.search(gemessen):
|
||||
gruende.append("Platzhalter statt Inhalt")
|
||||
grenze = WORTGRENZEN.get((bereich, feld))
|
||||
if grenze and len(zeile.split()) > grenze:
|
||||
if grenze and len(gemessen.split()) > grenze:
|
||||
gruende.append(f"Mehr als {grenze} Wörter")
|
||||
if bereich == "funktion" and feld == "titel":
|
||||
woerter = zeile.split()
|
||||
woerter = gemessen.split()
|
||||
if woerter and not _INFINITIV.search(woerter[-1]):
|
||||
gruende.append("Kein Verb im Infinitiv am Ende")
|
||||
# Der Titel nennt die Wirkung, nicht den Griff. „Befehl "make install"
|
||||
# ausführen" ist ein Testschritt — als Titel sagt er nicht, was danach gilt.
|
||||
if _TASTE.match(zeile):
|
||||
gruende.append("Auslöser statt Wirkung im Titel")
|
||||
if feld == "ziel":
|
||||
saetze = [s.strip() for s in re.split(r"[.!?]+", zeile) if s.strip()]
|
||||
saetze = [s.strip() for s in re.split(r"[.!?]+", gemessen) if s.strip()]
|
||||
if len(saetze) > ZIEL_SAETZE:
|
||||
gruende.append(f"Mehr als {ZIEL_SAETZE} Sätze")
|
||||
if any(len(s.split()) > ZIEL_WOERTER for s in saetze):
|
||||
@@ -100,7 +133,7 @@ def _umschreiben(bereich: str, feld: str, zeile: str) -> list[str]:
|
||||
|
||||
|
||||
def _handlung(zeile: str) -> bool:
|
||||
woerter = zeile.split()
|
||||
woerter = _messbar(zeile).split()
|
||||
if not woerter:
|
||||
return True
|
||||
return bool(_TASTE.match(zeile) or _INFINITIV.search(woerter[-1])
|
||||
@@ -115,10 +148,12 @@ def _zweiter_ablauf(zeilen: list[str]) -> str:
|
||||
„Bildergalerie bedienen" mit blättern, Miniatur wählen und Großansicht in einem.
|
||||
"""
|
||||
ergebnis = False
|
||||
for zeile in zeilen:
|
||||
for i, zeile in enumerate(zeilen):
|
||||
if ergebnis and _handlung(zeile):
|
||||
return zeile
|
||||
if not _handlung(zeile):
|
||||
# Die erste Zeile darf ein Zustand sein: das ist die Vorbedingung, nicht das
|
||||
# Ergebnis. „Badge ist sichtbar" vor „RMB auf Badge" ist ein Ablauf, nicht zwei.
|
||||
if i and not _handlung(zeile):
|
||||
ergebnis = True
|
||||
return ""
|
||||
|
||||
@@ -137,14 +172,15 @@ def _zeile(bereich: str, feld: str, zeile: str) -> list[dict]:
|
||||
def befunde(bereich: str, felder: dict) -> list[dict]:
|
||||
"""Alle messbaren Verstöße einer Prüfstelle, in Reihenfolge der Felder."""
|
||||
raus = []
|
||||
# Ein bloßer Titel beschreibt nichts — und lässt sich über alles stülpen. Der
|
||||
# Prüfer hielt damit einen Druckknopf für „Modal mit Tastatur steuern".
|
||||
# Testschritte sind Pflicht. Eine Funktion ist definiert als etwas, das man für
|
||||
# sich testen kann — ohne Ablauf ist die Behauptung unbelegt und der Titel lässt
|
||||
# sich über alles stülpen. Ziel und Regeln bleiben freiwillig.
|
||||
# Nur wenn die ganze Funktion vorliegt: bei einem einzelnen Feld sagt das Fehlen
|
||||
# der anderen nichts.
|
||||
inhaltsfelder = ("ziel", "regeln", "testen")
|
||||
if bereich == "funktion" and all(f in felder for f in inhaltsfelder) \
|
||||
and not any((felder.get(f) or "").strip() for f in inhaltsfelder):
|
||||
raus.append(_problem("testen", "", "Funktion ohne Ziel, Regeln und Testen"))
|
||||
and not (felder.get("testen") or "").strip():
|
||||
raus.append(_problem("testen", "", "Funktion ohne Testschritte"))
|
||||
return raus
|
||||
for feld, text in felder.items():
|
||||
if feld == "badge":
|
||||
|
||||
Reference in New Issue
Block a user