Trennstile: Update und Dokumentation, trenne_gn().
[wortliste.git] / skripte / python / edit_tools / stilfilter.py
blobe3d671d2268940a499fe784a1a85a648103d6de3
1 #!/usr/bin/env python
2 # -*- coding: utf8 -*-
3 # :Copyright: © 2018 Günter Milde.
4 # Released without warranty under the terms of the
5 # GNU General Public License (v. 2 or later)
6 # :Id: $Id: $
9 # Stilfilter
10 # ===========
12 # Funktionen, die einen Trennstil (oder einen Aspekt eines Trennstils)
13 # implementieren, z.B.
15 # >>> from stilfilter import syllabisch,morphemisch
16 # >>> print syllabisch(u"Pä-d<a-go-gik")
17 # Pä-da-go-gik
18 # >>> print morphemisch(u"Pä-d<a-go-gik")
19 # Päd<ago-gik
21 # (siehe auch dokumentation/Trennstile.txt).
22 # ::
24 """Filter für Trennstile"""
26 # Abhängigkeiten
27 # ==============
29 # Python 2.7 mit den Standardbibliotheken::
31 import re
33 # Wahltrennungen
34 # --------------
36 # Alternativtrennungen in Fremdwörtern nach §112 und §113.
38 # fremdwortsilben()
39 # ~~~~~~~~~~~~~~~~~
41 # Bei Konsonantenclustern mit l, n oder r wähle Trennung nach Herkunft.
43 # Regelwerk (1996) § 112:
44 # In Fremdwörtern können die Verbindungen aus Buchstaben für einen
45 # Konsonanten + l, n oder r entweder entsprechend § 110 getrennt werden,
46 # oder sie kommen ungetrennt auf die neue Zeile.
48 # >>> from stilfilter import fremdwortsilben
49 # >>> fremdwoerter = (u'no-b-le Zy-k-lus Ma-g-net Fe-b-ru-ar '
50 # ... u'Hy-d-rant Ar-th-ri-tis ge-r<i-a-t-risch '
51 # ... u'A·p-ri-ko-se')
52 # >>> for wort in fremdwoerter.split():
53 # ... print wort, '->', fremdwortsilben(wort)
54 # no-b-le -> no-ble
55 # Zy-k-lus -> Zy-klus
56 # Ma-g-net -> Ma-gnet
57 # Fe-b-ru-ar -> Fe-bru-ar
58 # Hy-d-rant -> Hy-drant
59 # Ar-th-ri-tis -> Ar-thri-tis
60 # ge-r<i-a-t-risch -> ge-r<i-a-trisch
61 # A·p-ri-ko-se -> A·pri-ko-se
63 # ::
65 def fremdwortsilben(wort):
66 """"fremdländische" Sprechsilben (no-ble, Hy-drant, Ma-gnet)."""
67 return re.sub(u'([-·][bcdfgkptv]|th)-(?=[lrn])', u'\\1', wort) # K86, K87
70 # standardsilben()
71 # ~~~~~~~~~~~~~~~~
73 # Bei Konsonantenclustern mit l, n oder r wähle Trennung nach deutschen Regeln.
75 # >>> from stilfilter import standardsilben
76 # >>> for wort in fremdwoerter.split():
77 # ... print wort, '->', standardsilben(wort)
78 # no-b-le -> nob-le
79 # Zy-k-lus -> Zyk-lus
80 # Ma-g-net -> Mag-net
81 # Fe-b-ru-ar -> Feb-ru-ar
82 # Hy-d-rant -> Hyd-rant
83 # Ar-th-ri-tis -> Arth-ri-tis
84 # ge-r<i-a-t-risch -> ge-r<i-at-risch
85 # A·p-ri-ko-se -> Ap-ri-ko-se
87 # ::
89 def standardsilben(wort):
90 """Standard-Sprechsilbentrennung (nob-le, Hyd-rant, Mag-net)."""
91 return re.sub(u'[-·]([bcdfgkptv]|th)-(?=[lrn])', u'\\1-', wort) # §112
93 # Tests:
95 # K86 Untrennbar sind in Fremdwörtern die Verbindungen von Verschluß- und
96 # Reibelauten mit l und r, ...
98 # >>> fremdwoerter = (u'Pu-b-li-kum flexi-b-ler Zy-k-lone Qua-d-rat '
99 # ... u'Spek-t-rum manö-v-rieren')
100 # >>> for wort in fremdwoerter.split():
101 # ... print wort, '->', fremdwortsilben(wort)
102 # Pu-b-li-kum -> Pu-bli-kum
103 # flexi-b-ler -> flexi-bler
104 # Zy-k-lone -> Zy-klone
105 # Qua-d-rat -> Qua-drat
106 # Spek-t-rum -> Spek-trum
107 # manö-v-rieren -> manö-vrieren
109 # "s-t-r" -> "s-tr"
111 # >>> fremdwoerter = u'Di<s-t-rikt Ma-gis-t-rat la-kus-t-risch'
112 # >>> for wort in fremdwoerter.split():
113 # ... print wort, '->', fremdwortsilben(wort)
114 # Di<s-t-rikt -> Di<s-trikt
115 # Ma-gis-t-rat -> Ma-gis-trat
116 # la-kus-t-risch -> la-kus-trisch
118 # K 87 Untrennbar ist die Konsonantenverbindung "gn".
120 # >>> fremdwoerter = u'Ma-g-net Pro-g-nose Si-g-net'
121 # >>> for wort in fremdwoerter.split():
122 # ... print wort, '->', fremdwortsilben(wort)
123 # Ma-g-net -> Ma-gnet
124 # Pro-g-nose -> Pro-gnose
125 # Si-g-net -> Si-gnet
127 # Keine Übergeneralisierung:
129 # >>> woerter = u'Seg-ler bast-le Ad-ler'
130 # >>> for wort in woerter.split():
131 # ... print wort, '->', fremdwortsilben(wort)
132 # Seg-ler -> Seg-ler
133 # bast-le -> bast-le
134 # Ad-ler -> Ad-ler
136 # Mit Auszeichnung der "Randtrennstelle":
137 # >>> woerter = u'A·p-ri-kose i·g-no-rie-ren'
138 # >>> for wort in woerter.split():
139 # ... print wort, '->', fremdwortsilben(wort)
140 # A·p-ri-kose -> A·pri-kose
141 # i·g-no-rie-ren -> i·gno-rie-ren
143 # regelsilben()
144 # ~~~~~~~~~~~~~
146 # Mit regelsilben() werden (auch nicht als Alternativtrennung ausgezeichnete)
147 # Fremdwortsilbentrennungen erkannt und zu Regelsilben gewandelt:
149 # >>> from stilfilter import regelsilben
150 # >>> fremdwoerter = (u'no-ble Zy-klus Ma-gnet Fe-bru-ar '
151 # ... u'Hy-drant Ar-thri-tis ge-r<i-a-trisch '
152 # ... u'A·pri-ko-se')
153 # >>> for wort in fremdwoerter.split():
154 # ... print wort, '->', regelsilben(wort)
155 # no-ble -> nob-le
156 # Zy-klus -> Zyk-lus
157 # Ma-gnet -> Mag-net
158 # Fe-bru-ar -> Feb-ru-ar
159 # Hy-drant -> Hyd-rant
160 # Ar-thri-tis -> Arth-ri-tis
161 # ge-r<i-a-trisch -> ge-r<i-at-risch
162 # A·pri-ko-se -> Ap-ri-ko-se
164 # ::
166 def regelsilben(wort):
167 """Regelsilben auch ohne Auszeichnung."""
168 wort = re.sub(u'[-·]([bcdfgkptv]|th)(?=[lr][aeiouäöü])', u'\\1-', wort)
169 wort = re.sub(u'[-·]gn(?=[aeiouäöü])', u'g-n', wort)
170 return wort
172 def trenne_gn(wort):
173 """Regelsilben bei gn (Si-gnal -> Sig-nal)."""
174 return re.sub(u'[-·]gn(?=[aeiouäöü])', u'g-n', wort)
177 # morphemisch()
178 # ~~~~~~~~~~~~~
180 # Entferne Alternativtrennungen nach §113 (verblasste Morphologie).
182 # Regelwerk (1996) §113:
183 # Wörter, die sprachhistorisch oder von der Herkunftssprache her gesehen
184 # Zusammensetzungen oder Präfigierungen sind, aber nicht mehr als solche
185 # empfunden oder erkannt werden, kann man entweder nach § 108 oder nach
186 # § 109 bis § 112 trennen.
188 # >>> from stilfilter import morphemisch
189 # >>> blasse = (u'hi-n<auf he-r<an da-r<um Chry-s<an-the-me Hek-t<ar '
190 # ... u'He-li-ko<p-ter in-te-r<es-sant Li-n<oleum Pä-d<a-go-gik '
191 # ... u'ge-r<i-a-t-risch au-to<ch-ton')
192 # >>> for wort in blasse.split():
193 # ... print wort, '->', morphemisch(wort)
194 # hi-n<auf -> hin<auf
195 # he-r<an -> her<an
196 # da-r<um -> dar<um
197 # Chry-s<an-the-me -> Chrys<an-the-me
198 # Hek-t<ar -> Hekt<ar
199 # He-li-ko<p-ter -> He-li-ko<pter
200 # in-te-r<es-sant -> in-ter<es-sant
201 # Li-n<oleum -> Lin<oleum
202 # Pä-d<a-go-gik -> Päd<ago-gik
203 # ge-r<i-a-t-risch -> ger<ia-t-risch
204 # au-to<ch-ton -> au-to<chton
207 # Ersetze, wenn zwischen Haupttrennstelle und Nebentrennstelle nur ein
208 # Buchstabe liegt.
209 # (Die Haupttrennstelle kann vor oder nach der Nebentrennstelle liegen.)
210 # ::
212 def morphemisch(wort):
213 """Trennung nach Morphologie (hin<auf, Hekt<ar Päd<ago-ge)."""
214 wort = re.sub(u'([<=]+[.]*(.|ch))[-.]+', u'\\1', wort)
215 wort = re.sub(u'[-.]+(.[<=]+)', u'\\1', wort)
216 return wort
218 # syllabisch()
219 # ~~~~~~~~~~~~
221 # >>> from stilfilter import syllabisch
222 # >>> for wort in blasse.split():
223 # ... print wort, '->', syllabisch(wort)
224 # hi-n<auf -> hi-nauf
225 # he-r<an -> he-ran
226 # da-r<um -> da-rum
227 # Chry-s<an-the-me -> Chry-san-the-me
228 # Hek-t<ar -> Hek-tar
229 # He-li-ko<p-ter -> He-li-kop-ter
230 # in-te-r<es-sant -> in-te-res-sant
231 # Li-n<oleum -> Li-noleum
232 # Pä-d<a-go-gik -> Pä-da-go-gik
233 # ge-r<i-a-t-risch -> ge-ri-a-t-risch
234 # au-to<ch-ton -> au-toch-ton
236 # ::
238 def syllabisch(wort):
239 """Ignoriere "verblasste" Morphologie (hi-nauf, Hek-tar, Pä-da-go-ge)."""
240 wort = re.sub(u'[<=]+[.]*((.|ch)[-.]+)', u'\\1', wort)
241 wort = re.sub(u'([-.]+.)[<=]+[.]*', u'\\1', wort)
242 return wort
245 # Tests:
247 # K44 „ſ“ (langes s) steht in Fremdwörtern...
249 # >>> blasse = (u'tran<s-pirieren tran<s-zendent ab<s-tinent '
250 # ... u'Ab<s-zess Pro-s<odie')
251 # >>> for wort in blasse.split():
252 # ... print wort, '->', morphemisch(wort)
253 # tran<s-pirieren -> tran<spirieren
254 # tran<s-zendent -> tran<szendent
255 # ab<s-tinent -> ab<stinent
256 # Ab<s-zess -> Ab<szess
257 # Pro-s<odie -> Pros<odie
259 # Trennstellen können als ungünstig markiert sein:
261 # >>> blasse = (u'Bür-ger=in<.i-ti-a-ti-ve Pä-..d<e-..rast')
262 # >>> for wort in blasse.split():
263 # ... print wort, '->', morphemisch(wort)
264 # Bür-ger=in<.i-ti-a-ti-ve -> Bür-ger=in<.iti-a-ti-ve
265 # Pä-..d<e-..rast -> Päd<erast
266 # >>> for wort in blasse.split():
267 # ... print wort, '->', syllabisch(wort)
268 # Bür-ger=in<.i-ti-a-ti-ve -> Bür-ger=ini-ti-a-ti-ve
269 # Pä-..d<e-..rast -> Pä-..de-..rast
271 # Suffixe mit Vokalcluster nicht ändern:
273 # >>> morphemisch(u'Zy-klo>i-de')
274 # u'Zy-klo>i-de'
277 # kombinierte Trennstile
278 # ----------------------
280 # ::
282 def etymologisch(wort):
283 """Etymologische Trennungen auch bei verblasster Etymologie."""
284 return morphemisch(fremdwortsilben(wort))
286 def modern(wort):
287 """Sprechsilbentrennungen bei verblasster Etymologie."""
288 return regelsilben(syllabisch(wort))
291 # Permissivität
292 # -------------
294 # Unterdrücken/Zulassen von regelkonformen Trennungen je nach Anwendungsfall.
295 # (Siehe Trennstile in sprachauszug.py.)
297 # notentext()
298 # ~~~~~~~~~~~
300 # Füge Trennmöglichkeiten am Wortanfang und -ende zu, die nach §107 E2
301 # des Regelwerkes (vorher K79) verboten sind aber in Notentexten gebraucht
302 # werden.
304 # >>> from stilfilter import notentext
305 # >>> print notentext(u'Abend')
306 # A·bend
307 # >>> print notentext(u'Ra-dio')
308 # Ra-di·o
310 # Das gleiche gilt für Trennmöglichkeiten am Anfang/Ende von Teilwörtern:
312 # >>> print notentext(u'Eis=ano-ma-lie')
313 # Eis=a·no-ma-lie
314 # >>> print notentext(u'Ra-dio<phon')
315 # Ra-di·o<phon
317 # Ausnahmen:
319 # >>> print notentext(u'Ai-chin-ger'), notentext(u'Ai-da')
320 # Ai-chin-ger A·i-da
321 # >>> print notentext(u'Ma-rie'), notentext(u'Li-nie')
322 # Ma-rie Li-ni·e
323 # >>> print notentext(u'Ta-too'), notentext(u'Zoo<lo-gie')
324 # Ta-too Zo·o<lo-gie
325 # >>> print notentext(u'A-pnoe'), notentext(u'O-boe')
326 # A-pnoe O-bo·e
327 # >>> print notentext(u'Plaque'), notentext(u'treue')
328 # Plaque treu·e
329 # >>> print notentext(u'Fon-due=pfan-ne'), notentext(u'Aue')
330 # Fon-due=pfan-ne Au·e
331 # >>> print notentext(u'Ge-nie'), notentext(u'Iphi-ge-nie')
332 # Ge-nie I·phi-ge-nie
333 # >>> print notentext(u'Ago-nie'), notentext(u'Be-go-nie')
334 # A·go-nie Be-go-ni·e
335 # >>> print notentext(u'Kom-pa-nie'), notentext(u'Kas-ta-nie'), notentext(u'Ge-ra-nie')
336 # Kom-pa-nie Kas-ta-ni·e Ge-ra-ni·e
338 # ungelöst: Knie / Kni·e # pl.
339 # ::
341 def notentext(word):
342 """Zusätzliche Trennmöglichkeiten am Wortrand (Fei-er=a·bend, Zo·o<lo-ge)."""
344 # Führender Vokal, gefolgt von Silbenanfang
345 # (optionaler Konsonant (auch ch/ck/ph/rh/sch/sh/th) + Vokal)::
347 for match in re.finditer(
348 u'(^|[<=])([aeiouäöü])((.|ch|ck|ph|sch|th)?[aeiouäöü])',
349 word, flags=re.IGNORECASE):
351 # Ausnahmen: Doppellaute, Diphtonge (außer A-i-da), Umlaute::
353 if (re.search(u'(aa|ae|ai|au|äu|ei|eu|oe|oo|ou|ue)',
354 match.group(0), flags=re.IGNORECASE)
355 and word != u'Ai-da'):
356 continue
358 # Ersetzen::
360 word = ''.join((word[:match.start()], match.expand(u'\\1\\\\3'),
361 notentext(word[match.end():])))
362 break
364 # zwei Vokale am Wortende::
366 for match in re.finditer(u'([aeiouäöü])([aeiouäöü])([<>=]|$)', word):
367 # if re.search(u'(oo)', match.group(0)):
368 # if 'ie' in match.group(0) and re.search(u'([a]-nie)', word, flags=re.IGNORECASE):
369 # sys.stderr.write(word+' '+match.group(0)+'\n')
371 # Ausnahme: Doppellaute, Diphtonge, Umlaute::
373 if (re.search(u'(aa|ae|ai|au|äu|ee|ei|eu|oe|oi|ou|ui)', match.group(0))
375 # Ausnahmen der Ausnahme::
377 and not word[:match.end()].endswith(u'waii') # ! Hawaii
378 and not word[:match.end()].endswith(u'boe')): # ! Oboe
379 continue
381 # Weitere Ausnahmen der Ausnahme::
383 # …oo außer zoo<, ...
384 if 'oo' in match.group(0) and match.group(0) != 'oo<':
385 continue
387 # …ie außer "-(l)inie", Iphigenie, Kastanie, Geranie, Begonie
388 if ('ie' in match.group(0)
389 and not word[:match.end()].endswith(u'i-nie') # Linie,
390 and not word[:match.end()].endswith(u'ta-nie') # Kastanie,
391 and not word[:match.end()].endswith(u'ra-nie') # Geranie,
392 and not word[:match.end()].endswith(u'e-go-nie') # Begonie != Agonie
393 and not word == u'Iphi-ge-nie'):
394 continue
396 # …ue (Plaque, Re-vue, vogue) außer "-tue, -aue, ... -äue"
397 if 'ue' in match.group(0) and re.search(u'[^aeät]ue([<>=]|$)',
398 word[:match.end()], flags=re.IGNORECASE):
399 continue
401 # Ersetzen::
403 word = ''.join((word[:match.start()], match.expand(u'\\\\2\\3'),
404 notentext(word[match.end():])))
405 break
407 return word
410 # keine_einzelvokale()
411 # ~~~~~~~~~~~~~~~~~~~~
413 # Traditionell enthalten die TeX-Trennmuster keine Trennstellen deren Abstand
414 # zur Nachbartrennstelle einen Buchstaben beträgt. Dies ist eine „ästhetische“
415 # Entscheidung um „Flatterbuchstaben“ zu vermeiden
417 # Bei einvokalischen Silben im Wortinneren, nimm die zweite:
419 # >>> from stilfilter import keine_einzelvokale
420 # >>> einzelne = (u'The-a-ter me-ri-di-.o-nal')
421 # >>> for wort in einzelne.split():
422 # ... print wort, '->', keine_einzelvokale(wort)
423 # The-a-ter -> Thea-ter
424 # me-ri-di-.o-nal -> me-ri-dio-nal
426 # Allerdings nicht, wenn die zweite Trennstelle unterdrückt ist:
428 # >>> einzelne = (u'La-sal-le-a-.ner Athe-i.sten')
429 # >>> for wort in einzelne.split():
430 # ... print wort, '->', keine_einzelvokale(wort)
431 # La-sal-le-a-.ner -> La-sal-le-a-.ner
432 # Athe-i.sten -> Athe-i.sten
434 # ::
436 def keine_einzelvokale(wort):
437 """Entferne Trennmarker vor Einzevokalsilben (Thea-ter)."""
438 wort = re.sub(u'>([aeiouyäöü])-', u'>\\1', wort)
439 wort = re.sub(u'-[.]*([aeiouyäöü])(?=[->][^.])', u'\\1', wort)
440 return wort
443 # unguenstig()
444 # ~~~~~~~~~~~~
446 # Entferne die explizit als ungünstig gekennzeichneten Trennstellen:
448 # >>> from stilfilter import unguenstig
449 # >>> unguenstig(u'Text=il<..lu-stra-ti-.on')
450 # u'Text=illu-stra-tion'
451 # >>> unguenstig(u'Text=il<..lu-stra-ti-.on', level=2)
452 # u'Text=illu-stra-ti-on'
454 # ::
456 def unguenstig(wort, level=1):
457 """Entferne als ungünstig markierte Trennungen."""
458 marker = r'\.' * level
459 wort = re.sub(u'[-=<>]+%s+'%marker, u'', wort)
460 return wort.replace(u'.', u'')
462 # standard()
463 # ~~~~~~~~~~
465 # Alias für normale Trennhäufigkeit::
467 standard = unguenstig
469 # inklusiv()
470 # ~~~~~~~~~~
471 # ::
472 def inklusiv(wort):
473 """Zulassen aller Trennungen (auch ungünstige)."""
474 return unguenstig(wort, level=10)
476 # permissiv()
477 # ~~~~~~~~~~~
478 # Aussortieren sehr ungünstiger (mit „..“ markierter) Trennstellen.
479 # ::
481 def permissiv(wort):
482 """Aussortieren sehr ungünstiger (mit „..“ markierter) Trennstellen."""
483 return unguenstig(wort, level=2)
485 # fix()
486 # ~~~~~
488 # Standardunterdrückung + keine Einzelvokale::
490 def fix(wort):
491 """Entferne ungünstige Trennungen und Einvokalsilben."""
492 return keine_einzelvokale(unguenstig(wort))
494 # buchdruckerregel()
495 # ~~~~~~~~~~~~~~~~~~
497 # Nach alter Buchdruckerregel werden zwei Buchstaben am Wortanfang oder -ende
498 # nicht abgetrennt:
500 # >>> from stilfilter import buchdruckerregel
501 # >>> buchdruckerregel(u'Il<lu-stra-tion')
502 # u'Illu-stra-tion'
503 # >>> buchdruckerregel(u'An<ga-be')
504 # u'Angabe'
505 # >>> buchdruckerregel(u'dar<an=mach-te')
506 # u'daran=machte'
507 # >>> buchdruckerregel(u'ab<zu<be<ru-fen')
508 # u'abzu<beru-fen'
510 # TODO: Günstigste Trennung bei Zusammensetzungen mit ..<..< ::
512 # >>> buchdruckerregel(u'ab<be<ru-fen')
513 # u'abbe<rufen'
514 # >>> buchdruckerregel(u'zu<zu<be<rei-ten')
515 # u'zuzu<berei-ten'
516 # >>> print buchdruckerregel(u'un<zu<ver<läs-sig')
517 # unzu<ver<läs-sig
519 # ::
521 def buchdruckerregel(wort):
522 """Experimentell: Entferne Trennungen im Abstand 2 vom Wortrand."""
523 wort = re.sub(u'^(..)[-<>.]+', u'\\1', wort)
524 wort = re.sub(u'[-<>.]+(..)$', u'\\1', wort)
525 wort = re.sub(u'[-<>.]*(..=+..)[-<>.]*', u'\\1', wort)
526 wort = re.sub(u'[-<>.]*(..<..)[-<>.]*', u'\\1', wort)
527 return wort