1 -- -*- coding: utf-8 -*-
4 Copyright 2012, 2013 Stephan Hennig
6 This program is free software: you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the
8 Free Software Foundation, either version 3 of the License, or (at your
9 option) any later version.
11 This program is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
16 You should have received a copy of the GNU General Public License along
17 with this program. If not, see <http://www.gnu.org/licenses/>.
22 --- Dieses Modul stellt die folgende Funktionalität zur Manipulation der
26 -- <li>Prüfen von Wörtern auf Wohlgeformtheit,</li>
27 -- <li>Normalisieren von Wörtern (Übertragen in ein für <a href="http://tug.org/docs/liang/">Patgen</a> geeignetes Format):<br />
28 -- <code>Lei-nen==be[t=tu-/{tt/tt=t}u.]ches</code>  →  <code>Lei-nen-bettuches</code>,</li>
33 -- @author Stephan Hennig
34 -- @copyright 2012, 2013, Stephan Hennig
36 -- Die API-Dokumentation kann mit <pre>
38 -- luadoc -d API *.lua
40 -- </pre> erstellt werden.
44 --[[ just for luadoc 3.0 compatibility
47 -- lokale Modul-Tabelle
53 local lpeg
= require("lpeg")
54 local unicode
= require("unicode")
58 -- Kürzel für Funktionen der Standardbibliotheken.
59 local Tconcat
= table.concat
60 -- Kürzel für LPEG-Funktionen.
65 -- Kürzel für Unicode-Funktionen.
66 local Ufind
= unicode
.utf8
.find
67 local Ugsub
= unicode
.utf8
.gsub
68 local Ulen
= unicode
.utf8
.len
69 local Umatch
= unicode
.utf8
.match
70 local Usub
= unicode
.utf8
.sub
75 -- Hilfsfunktionen und -variablen für LPEG-Mustersuche.
77 -- Eine Tabelle, in der während der Mustersuche Eigenschaften des
78 -- untersuchten Wortes gespeichert werden.
81 --- <strong>nicht-öffentlich</strong> Diese Funktion wird zu Beginn
82 --- einer erfolgreichen Prüfung der Wortgrammatik gegen einen String
84 -- Initialisiert Tabelle mit Worteigenschaften.
85 local function _at_word_start()
89 --- <strong>nicht-öffentlich</strong> Diese Funktion wird am Ende einer
90 --- erfolgreichen Prüfung der Wortgrammatik gegen einen String
92 -- Verwirft alle ausstehenden Captures.
94 -- @param ... Variable Anzahl an Strings
96 -- @return Tabelle mit Worteigenschaften
97 local function _at_word_end(...)
98 word_property
.norm_word
= Tconcat({...})
102 --- <strong>nicht-öffentlich</strong> Merke Eigenschaft
103 --- 'Spezialtrennung enthalten'.
104 -- Die Capture wird nicht verändert.
109 local function _property_has_nonstd(c
)
110 word_property
.has_nonstd
= true
114 --- <strong>nicht-öffentlich</strong> Merke Eigenschaft
115 --- 'Dreikonsonantenregel für s enthalten'.
116 -- Die Capture wird nicht verändert.
121 local function _property_has_nonstd_sss(c
)
122 word_property
.has_nonstd_sss
= true
126 --- <strong>nicht-öffentlich</strong> Merke Eigenschaft 'ß enthalten'.
127 local function _property_has_eszett()
128 word_property
.has_eszett
= true
131 --- <strong>nicht-öffentlich</strong> Füge eine beliebige Zahl von
132 --- Strings zusammen.
133 -- Diese Funktion wird in einer Funktionscapture genutzt, um Cluster
134 -- zusammenzufügen. Vorhandene normalisierte Trennzeichen werden aus
135 -- der Ergebniscapture entfernt.
137 -- @param ... Variable Anzahl an Strings
139 -- @return zusammengesetzter String
140 local function _concatenate_alt_part_word(...)
141 -- io.stderr:write("conc: ",Tconcat({...}, ","),"\n")-- For debugging purposes.
142 return ( Ugsub(Tconcat({...}), "-", "") )
145 --- <strong>nicht-öffentlich</strong> Prüfe zwei Alternativen auf
147 -- Diese Funktion wird in einer Funktionscapture verwendet, um die
148 -- Gleichheit zweier Alternativen zu prüfen. Falls die Alternativen
149 -- ungleich sind, so wird dies als Worteigenschaft vermerkt. Ergebnis
150 -- ist das erste Argument.
152 -- @param a Capture 1
154 -- @param b Capture 2
157 local function _is_equal_two_alternatives(a
, b
)
158 word_property
.has_invalid_alt
= word_property
.has_invalid_alt
or (a
~= b
)
165 -- Die Grammatik eines Wortes.
167 -- Diese Grammatik beschreibt die Struktur zulässiger Wörter. Durch
168 -- Prüfen eines Strings gegen diese Grammatik kann festgestellt werden,
169 -- ob dieser ein Wort im Sinne der Grammatik enthält.
171 -- Bei positivem Ergebnis ist der Rückgabewert der Funktion `match` eine
172 -- Tabelle, in welcher während der Prüfung die folgenden Eigenschaften
173 -- des betrachteten Wortes gespeichert werden.
177 -- has_invalid_alt Alternativen sind nicht identisch
178 -- has_eszett Wort enthält Buchstaben 'ß'
179 -- has_nonstd Wort enthält Spezialtrennung
180 -- has_nonstd_sss Wort enthält Spezialtrennung für das 's'
181 -- norm_word normalisiertes Wort
183 -- Die Grammatik beschreibt die Zulässigkeit von Wörtern nicht
184 -- erschöpfend. So wird beispielsweise nicht darauf eingegangen, ob
185 -- Trennstellen einen Mindestabstand vom Wortrand haben müssen. Die
186 -- Zulässigkeit von Wörtern sollten daher mit zusätzlichen,
187 -- nachgeschalteten Prüfungen festgestellt werden (siehe Funktion
188 -- `validate_word()`).
190 -- Weitere Informationen können der Datei `README.wortliste` entnommen
199 -- Am Anfang der Prüfung wird die Tabelle mit Worteigenschaften
200 -- zurückgesetzt. Am Ende werden alle ausstehenden Captures
202 property_table
= P(true) / _at_word_start
* V
"word" * -1 / _at_word_end
,
203 -- Ein Wort beginnt mit einem Wortanfang. Darauf folgt ein Wortrest.
204 word
= V
"word_head" * V
"word_tail",
205 -- Ein Wortanfang besteht aus einem Kluster.
206 word_head
= V
"cluster",
207 -- Ein Wortrest besteht aus einer normalisierten
208 -- Trennstellenmarkierung gefolgt von einem Kluster. Der gesamte
209 -- Ausdruck ist optional.
210 word_tail
= (V
"norm_hyphen" * V
"cluster")^
0,
214 -- Die Markierung von Trennstellen erfolgt entweder ausschließlich
215 -- nach deren morphologischer Struktur. Alternativ kann eine
216 -- Trennstelle auch qualitativ bewertet werden, optional mit
217 -- vorangestellter, morphologischer Markierung.
219 -- Normalisierte Trennstelle
221 -- Während des Normalisierens werden die Zeichen einer
222 -- Trennstellenmarkierung in ein für Patgen geeignetes Zeichen
225 -- Es gibt zwei Arten von normalisierten Trennstellenmarkierungen mit
226 -- unterschiedlichen Captures.
227 norm_hyphen
= V
"hyphen_norm_without_quality" + V
"hyphen_norm_with_quality",
228 -- Trennstellenmarkierungen ohne Bewertung werden in ein einfaches
229 -- Trennzeichen umgewandelt.
230 hyphen_norm_without_quality
= V
"hyphen_without_quality" / "-",
231 -- Trennstellmarkierungen mit Bewertung werden in einen Leerstring
232 -- gewandelt; solche Trennungen werden also unterdrückt.
233 hyphen_norm_with_quality
= V
"hyphen_with_quality" / "",
234 -- Eine Trennstelle mit Bewertung kann eine optionale, vorstehende,
235 -- morphologische Markierung enthalten.
236 hyphen_with_quality
= V
"hyphen_morph"^
0 * V
"hyphen_quality",
237 -- Eine Trennstelle ohne Bewertung ist ausschließlich morphologisch
239 hyphen_without_quality
= V
"hyphen_morph" * -V
"hyphen_quality",
241 -- morphologische Markierungen
243 -- innerhalb von Wortstämmen, Präfixen oder Suffixen
244 hyphen_inner
= V
"hyphen_ch_inner"^
1,
245 -- nach Präfixen oder Verbalpartikeln
246 hyphen_prefix
= V
"hyphen_ch_prefix"^
1,
248 hyphen_suffix
= V
"hyphen_ch_suffix"^
1,
250 hyphen_compound
= V
"hyphen_ch_compound"^
1,
251 -- nach Präfix eines zusammengesetzten Wortes
252 hyphen_compound_prefix
= V
"hyphen_ch_prefix" * V
"hyphen_compound",
253 -- vor Suffix eines zusammengesetzten Wortes
254 hyphen_compound_suffix
= V
"hyphen_compound" * V
"hyphen_ch_suffix",
256 hyphen_uncategorized
= V
"hyphen_ch_uncategorized",
257 -- eine morphologische Markierung (Achtung: In der folgenden Regel
258 -- ist die Reihenfolge der Prüfung relevant. Gemischte Trennzeichen
259 -- müssen vor reinen Trennzeichen geprüft werden.)
261 V
"hyphen_compound_prefix"
262 + V
"hyphen_compound_suffix"
267 + V
"hyphen_uncategorized"
270 -- qualitative Markierungen (Bewertung)
272 -- Eine Bewertung besteht aus ein bis drei Bewertungszeichen.
273 hyphen_quality
= V
"hyphen_ch_quality" * V
"hyphen_ch_quality"^
-2,
274 -- Eine beliebige Trennstellenmarkierung.
275 hyphen
= V
"hyphen_without_quality" + V
"hyphen_with_quality",
276 -- Die folgenden Zeichen werden zur Trennstellenmarkierung verwendet:
277 hyphen_ch_inner
= P
"-",
278 hyphen_ch_prefix
= P
"<",
279 hyphen_ch_suffix
= P
">",
280 hyphen_ch_compound
= P
"=",
281 hyphen_ch_quality
= P
".",
282 hyphen_ch_uncategorized
= P
"·",
286 -- Ein Kluster besteht aus einem oder mehreren aufeinanderfolgenden
287 -- `fundamentalen Klustern` ohne Unterbrechung durch Trennzeichen.
288 -- Es gibt drei Arten von fundamentalen Klustern:
290 -- * Buchstabenkluster,
291 -- * Ausdrücke für Alternativen,
292 -- * Ausdrücke für Spezialtrennungen,
294 -- Bü-cher E{ck/k-k}e Sto{ff/ff-f}et-zen Wach[-s/s-]tu-be
295 -- Kluster 11-2222 1111111111 11111111111111-222 1111111111111-22
296 -- fund. Kluster 11-2222 1222222223 11122222222233-444 1111222222233-44
298 cluster
= (V
"cl_letter" + V
"cl_nonstd" + V
"cl_alt")^
1,
302 -- Ein Buchstabenkluster besteht aus aufeinanderfolgenden Buchstaben.
303 -- Die Capture sammelt alle Buchstaben.
304 cl_letter
= C(V
"letter"^
1),
305 -- Die folgende Liste der zulässigen Buchstaben sollte mit der
306 -- Translate-Datei daten/german.tr für Patgen synchron gehalten
307 -- werden. Die explizite Liste der akzentuierten und
308 -- Sonderbuchstaben wurde mit dem Skript tr2lpeg.awk aus der Datei
309 -- german.tr erstellt. Dieser Vorgang ist bei Änderung der Datei
310 -- german.tr zu wiederholen. Zum Aufruf siehe tr2lpeg.awk.
312 -- Die folgenden Buchstaben sind zulässig.
313 letter
= R("az", "AZ")
338 + P
"ß" / _property_has_eszett
346 -- Ausdrücke für Alternativen
348 -- Eine Alternative beschreibt mehrdeutige Wortteile, die
349 -- unterschiedlich getrennt werden können.
351 -- Ausdrücke für Alternativen werden in begrenzende Zeichen
352 -- (Klammern) eingeschlossen.
353 cl_alt
= V
"alt_open" * V
"alt_rule" * V
"alt_close",
354 -- Teilausdrücke einer Alternative werden durch ein spezielles
355 -- Zeichen voneinander getrennt. Beide Alternativen werden auf
356 -- Buchstabengleichheit geprüft. Die Capture enthält abschließend
357 -- die Buchstaben der ersten Alternative.
358 alt_rule
= (V
"alt_1st" * V
"alt_sep" * V
"alt_2nd") / _is_equal_two_alternatives
,
359 -- Alternativen enthalten Alternativteilwörter. Die Unterschiede
360 -- zwischen Wörtern und Alternativteilwörtern sind:
362 -- * Alternativteilwörter können leer sein.
363 -- * Sie können eine führende und/oder abschließende
364 -- Trennstellenmarkierung haben.
365 -- * Die Capture von Alternativteilwörter enthält keine Trennzeichen.
366 alt_1st
= V
"alt_part_word",
367 alt_2nd
= V
"alt_part_word",
368 -- Ein Alternativteilwort ist ein Wort, welches eine führende und
369 -- abschließende Trennstellenmarkierung enthalten kann. Oder es ist
370 -- ein leeres Wort, wahlweise mit einem Trennzeichen. Die
371 -- Reihenfolge der Prüfung beider Möglichkeiten ist relevant. Auf
372 -- ein leeres Wort darf erst zuletzt geprüft werden.
373 alt_part_word
= V
"ophyphen" * V
"word" / _concatenate_alt_part_word
* V
"ophyphen"
376 -- Eine optionale Trennstellenmarkierung.
377 ophyphen
= V
"hyphen"^
-1,
378 -- Zeichen, welches einen Ausdruck für Alternativen einführt.
380 -- Zeichen, welches einen Ausdruck für Alternativen abschließt.
382 -- Zeichen, welches Teilausdrücke einer Alternative voneinander
386 -- Ausdrücke für Spezialtrennungen
388 -- Spezialtrennungen (non-standard hyphenation) beschreiben
389 -- Trennregeln, die über das bloße Einfügen eines Trennzeichens
390 -- hinausgehen. Dazu gehören die ck-Trennung und die
391 -- Dreikonsonantenregel(n). Spezialtrennungen sind jeweils
392 -- eingeschlossen in Klammern. Die Capture enthält eine
393 -- Zeichenkette, die der jeweils ungetrennten Spezialtrennung
394 -- entspricht (z. B. 'ck' für die ck-Trennung).
396 -- Ausdrücke für Spezialtrennungen werden in begrenzende Zeichen
397 -- (Klammern) eingeschlossen.
398 cl_nonstd
= V
"nonstd_open" * V
"nonstd_rule" * V
"nonstd_close",
399 -- Aufzählung sämtlicher Spezialtrennungen. Das Auftreten von
400 -- Spezialtrennungen wird als Worteigenschaft vermerkt.
401 nonstd_rule
= (V
"ck" + V
"bbb" + V
"fff" + V
"lll" + V
"mmm" + V
"nnn" + V
"ppp" + V
"rrr" + V
"ttt" + V
"nonstd_sss") / _property_has_nonstd
,
402 -- ck-Trennung: Die Capture enthält die Zeichenfolge 'ck'.
403 ck
= C(P
"ck") * V
"nonstd_sep" * P
"k" * V
"hyphen" * P
"k",
404 -- Dreikonsonantenregel: Die Capture enthält die Zeichenfolge für die
405 -- ungetrennte Konsonantenfolge.
406 bbb
= C(P
"bb") * V
"nonstd_sep" * P
"bb" * V
"hyphen" * P
"b",
407 fff
= C(P
"ff") * V
"nonstd_sep" * P
"ff" * V
"hyphen" * P
"f",
408 lll
= C(P
"ll") * V
"nonstd_sep" * P
"ll" * V
"hyphen" * P
"l",
409 mmm
= C(P
"mm") * V
"nonstd_sep" * P
"mm" * V
"hyphen" * P
"m",
410 nnn
= C(P
"nn") * V
"nonstd_sep" * P
"nn" * V
"hyphen" * P
"n",
411 ppp
= C(P
"pp") * V
"nonstd_sep" * P
"pp" * V
"hyphen" * P
"p",
412 rrr
= C(P
"rr") * V
"nonstd_sep" * P
"rr" * V
"hyphen" * P
"r",
413 ttt
= C(P
"tt") * V
"nonstd_sep" * P
"tt" * V
"hyphen" * P
"t",
414 sss
= C(P
"ss") * V
"nonstd_sep" * P
"ss" * V
"hyphen" * P
"s",
415 -- Das Auftreten der Dreikonsonantenregel für 's' wird als
416 -- Worteigenschaft vermerkt.
417 nonstd_sss
= V
"sss" / _property_has_nonstd_sss
,
418 -- Zeichen, welches einen Ausdruck für Spezialtrennungen einführt.
420 -- Zeichen, welches einen Ausdruck für Spezialtrennungen abschließt.
422 -- Zeichen, welches Teilausdrücke einer Spezialtrennung voneinander
429 --- Prüfe String gegen Wortgrammatik.
430 -- Falls der String ein zulässiges Wort darstellt, wird eine Tabelle mit
431 -- Eigenschaften des Wortes zurückgegeben. Die Tabelle enthält im Feld
432 -- `norm_word` das Wort in einem für Patgen geeigneten Format.
433 -- Sämtliche Trennzeichen werden durch '<code>-</code>' ersetzt.
434 -- Spezialtrennungen und Alternativen werden zur ersten Altenative
437 -- @param rawword unbehandeltes Wort
439 -- @return <code>nil</code>, falls das Wort eine unzulässige Struktur
440 -- hat;<br /> eine Tabelle mit Eigenschaften des betrachteten Wortes,
442 local function normalize_word(rawword
)
443 -- Prüfung der Wortstruktur und Ermittlung der Worteigenschaften.
444 return word
:match(rawword
)
446 M
.normalize_word
= normalize_word
450 --- Prüfe ein Wort auf Wohlgeformtheit.
452 -- @param rawword Wort (normiert oder unbehandelt)
454 -- @return <code>nil, msg</code>, falls das Wort eine unzulässige
455 -- Struktur hat;<br /> eine Tabelle mit Eigenschaften des betrachteten
456 -- Wortes, sonst.<br /> <code>msg</code> ist ein String, der die
457 -- Unzulässigkeit näher beschreibt.
458 local function validate_word(rawword
)
459 -- Normalisiere Wort.
460 local props
= normalize_word(rawword
)
462 if not props
then return nil, "ungültiges Wort" end
463 -- Prüfe auf unzulässige Alternativen.
464 if props
.has_invalid_alt
then return nil, "ungleiche Alternativen" end
465 -- Ermittle normalisiertes Wort.
466 local word
= props
.norm_word
467 -- Prüfe minimale Wortlänge.
468 local len
= Ulen(Ugsub(word
, "-", ""))
469 if len
< 3 then return nil, "weniger als drei Buchstaben" end
470 if len
== 3 and not props
.has_eszett
then return nil, "dreibuchstabig ohne Eszett" end
471 -- Prüfe Wortenden auf unzulässige Trennzeichen.
472 local len
= Ulen(word
)
474 local ch
= Usub(word
, 2, 2)
476 -- Randtrennungen sind im Notentext zulässig.
478 -- Achtung: Unsaubere Implementierung! Dem Test auf
479 -- Randtrennungen für Notentext steht die Normalisierung aller
480 -- Trennzeichen (Umwandlung in das Zeichen -) in
481 -- normalize_word() entgegen. Hier wird daher das originale
482 -- Wort rawword geprüft.
484 -- Idee: Sinnvoller wäre es, die Grammatik eines Wortes so zu
485 -- erweitern, dass ein zusammengesetztes Wort aus, gegebenfalls
486 -- affigierten, einfachen oder zusammengesetzten Wörtern besteht,
487 -- die durch Wortfugentrennzeichen = verbunden sind.
488 -- Zusammengesetzte Wörter können wiederum affigiert sein
489 -- (Trennzeichen <, >, <=, =>). Einfache Wörter und Affixe können
490 -- Randtrennungen · enthalten. Randtrennzeichen werden bei der
491 -- Normalisierung entfernt (statt sie durch - zu ersetzen).
492 -- Eventuell kann auch die Länge von Buchstabenklustern um
493 -- (Nicht-)Randtrennzeichen innerhalb der Grammatik geprüft
494 -- werden. Auf diese Weise müsste hier nicht auf Wissen über die
495 -- Syntax von Trennzeichen zurückgegriffen werden bzw. die
496 -- nachträgliche Prüfung auf unzulässige, randnahe Trennzeichen
499 -- Extrahiere Trennstellenmarkierung aus Originalwort ab
501 local rch
= Umatch(rawword
, "^.([<>=.·-]+)")
502 -- Keine explizite Randtrennung?
503 if rch
and not Ufind(rch
, "·") then
504 return nil, "Trennzeichen am Wortanfang"
508 local ch
= Usub(word
, len
-1, len
-1)
510 -- Extrahiere Trennstellenmarkierung aus Originalwort, die bis zur
511 -- vorletzten Position reicht.
512 local rch
= Umatch(rawword
, "([<>=.·-]+).$")
513 -- Keine explizite Randtrennung?
514 if rch
and not Ufind(rch
, "·") then
515 return nil, "Trennzeichen am Wortende"
520 M
.validate_word
= validate_word
524 -- Exportiere Modul-Tabelle.