plint

French poetry validator (local mirror of https://gitlab.com/a3nm/plint)
git clone https://a3nm.net/git/plint/
Log | Files | Refs | README

commit d4a587ffe950b9d19bcff0cd80d843972da7744d
parent 0301ba5432fb3096ef5da2aabb4cb9770b16cb41
Author: Antoine Amarilli <a3nm@a3nm.net>
Date:   Sun, 27 Jul 2014 18:18:40 +0200

disallow poor rhymes which are also poor pour l'œil

Diffstat:
TODO | 4++++
error.py | 5+++--
options.py | 3+++
rhyme.py | 62+++++++++++++++++++++++++++++++++++++++++++++++++++++---------
template.py | 39++++++++++++++++++++++++++-------------
verse.py | 15+++++++++++++++
views/about.html | 13+++++++++++++
7 files changed, 117 insertions(+), 24 deletions(-)

diff --git a/TODO b/TODO @@ -1,4 +1,8 @@ - profiling +- option to forbid rhyme with only "-ment" except for "doublement", + "triplement", "-uplement", and "complètement" -- not with boileau +- document "poor_adverb_ok" +- change additions to give numerical syllable count - highlight possible hemistiche positions where an hemistiche could have been placed - document expected errors of test.sh and commit necessary files - remove kludge for invalid characters, split them in specific chunks diff --git a/error.py b/error.py @@ -88,9 +88,10 @@ class ErrorHiatus(ErrorBadElement): key = "hiatus" class ErrorBadRhyme: - def __init__(self, expected, inferred): + def __init__(self, expected, inferred, old_phon=None): self.expected = expected self.inferred = inferred + self.old_phon = old_phon def report(self, pattern): return (_("%s for type %s (expected %s, inferred %s)") @@ -130,7 +131,7 @@ class ErrorBadRhymeEye(ErrorBadRhymeObject): return _("Bad rhyme ending") def fmt(self, l): - return "\"-" + l.sufficient_eye() + "\"" + return "\"-" + l.sufficient_eye(self.old_phon) + "\"" class ErrorBadMetric: def report(self, pattern): diff --git a/options.py b/options.py @@ -9,5 +9,8 @@ default_options = { 'incomplete_ok': True, 'check_end_hemistiche': True, 'check_occurrences': True, + 'poor_eye_required': True, + 'poor_eye_supposed_ok': False, + 'poor_adverb_ok': False, 'diaeresis': "classical" } diff --git a/rhyme.py b/rhyme.py @@ -63,7 +63,7 @@ class Rhyme: def supposed_liaison(self, x): if x[-1] in liaison.keys() and self.options['eye_supposed_ok']: - return x + liaison[x[-1]] + return x[:-1] + liaison[x[-1]] return x def __init__(self, line, constraint=None, mergers=None, options=None, phon=None): @@ -72,6 +72,9 @@ class Rhyme: else: self.constraint = Constraint() self.mergers = {} + # length of smallest end-of-verse word in syllables + # will be provided later + self.last_count = 42 if options: self.options = options else: @@ -84,13 +87,18 @@ class Rhyme: phon = self.lookup(line) self.phon = set([self.apply_mergers(x) for x in phon]) self.eye = self.supposed_liaison(self.consonant_suffix(line)) + self.raw_eye = line self.old_phon = None self.old_eye = None + self.old_raw_eye = None + self.old_last_count = None self.new_rhyme = None def rollback(self): self.phon = self.old_phon self.eye = self.old_eye + self.raw_eye = self.old_raw_eye + self.last_count = self.old_last_count def sufficient_phon(self): # return the shortest accepted rhymes among old_phon @@ -105,10 +113,27 @@ class Rhyme: ok.add(p[-slen:]) return ok - def sufficient_eye(self): - return self.eye[-1] + def sufficient_eye_length(self, old_phon=None): + if not self.constraint.classical: + return self.eye, 0 # not classical, nothing required + if ((old_phon >= 2 if old_phon else self.satisfied_phon(2)) + or not self.options['poor_eye_required']): + return self.eye, 1 + if self.last_count == 1: + return self.eye, 1 + if self.options['poor_eye_supposed_ok']: + return self.eye, 2 + else: + return self.raw_eye, 2 + + def sufficient_eye(self, old_phon=None): + d, val = self.sufficient_eye_length(old_phon) + if val <= len(d): + return d[-val:] + else: + return d - def match(self, phon, eye): + def match(self, phon, eye, raw_eye): """limit our phon and eye to those which match phon and eye and which respect constraints""" new_phon = set() @@ -124,16 +149,28 @@ class Rhyme: self.eye = "" else: self.eye = self.eye[-val:] + if self.raw_eye: + val = eye_rhyme(self.raw_eye, raw_eye) + if val == 0: + self.raw_eye = "" + else: + self.raw_eye = self.raw_eye[-val:] + + def adjustLastCount(self, v): + self.last_count = min(self.last_count, v) def restrict(self, r): """take the intersection between us and rhyme object r""" if self.satisfied(): self.old_phon = self.phon self.old_eye = self.eye + self.old_last_count = self.last_count + self.old_raw_eye = self.raw_eye + # lastCount will be applied later self.constraint.restrict(r.constraint) self.new_rhyme = r self.match(set([self.apply_mergers(x) for x in r.phon]), - self.supposed_liaison(self.consonant_suffix(r.eye))) + self.supposed_liaison(self.consonant_suffix(r.eye)), r.raw_eye) def consonant_suffix(self, s): if not self.options['eye_tolerance_ok']: @@ -145,13 +182,20 @@ class Rhyme: def feed(self, line, constraint=None): """extend us with a line and a constraint""" + # lastCount is not applied yet return self.restrict(Rhyme(line, constraint, self.mergers, self.options)) - def satisfied_phon(self): - return len(self.phon) >= self.constraint.phon + def satisfied_phon(self, val=None): + if not val: + val = self.constraint.phon + for x in self.phon: + if len(x) >= val: + return True + return False def satisfied_eye(self): - return (len(self.eye) > 0 or not self.constraint.classical) + d, l = self.sufficient_eye_length() + return len(d) >= l def satisfied(self): return self.satisfied_phon() and self.satisfied_eye() @@ -235,7 +279,7 @@ if __name__ == '__main__': constraint = Constraint() rhyme = Rhyme(line[0], constraint, self.mergers, self.options) for x in line[1:]: - rhyme.feed(x) + rhyme.feed(x, 42) rhyme.pprint() if not rhyme.satisfied(): print("No.") diff --git a/template.py b/template.py @@ -49,7 +49,10 @@ class Template: 'incomplet_ok': 'incomplete_ok', 'phon_supposee_ok': 'phon_supposed_ok', 'oeil_supposee_ok': 'eye_supposed_ok', - 'oeil_tolerance_ok': 'eye_tolerance_ok' + 'oeil_tolerance_ok': 'eye_tolerance_ok', + 'pauvre_oeil_requise': 'poor_eye_required', + 'pauvre_oeil_supposee_ok': 'poor_eye_supposed_ok', + 'pauvre_adverbe_ok': 'poor_adverb_ok', } @@ -121,25 +124,22 @@ class Template: if self.overflowed: return [error.ErrorOverflowedTemplate()], pattern, v + rhyme_failed = False # rhymes if pattern.myid not in self.env.keys(): # initialize the rhyme - self.env[pattern.myid] = rhyme.Rhyme(v.normalized, pattern.constraint, - self.mergers, self.options) + # last_count is passed later + self.env[pattern.myid] = rhyme.Rhyme(v.normalized, + pattern.constraint, self.mergers, self.options) else: # update the rhyme self.env[pattern.myid].feed(v.normalized, pattern.constraint) - if not self.env[pattern.myid].satisfied(): - # no more possible rhymes, something went wrong - phon_ok = self.env[pattern.myid].satisfied_phon() - eye_ok = self.env[pattern.myid].satisfied_eye() + if not self.env[pattern.myid].satisfied_phon(): + # no more possible rhymes, something went wrong, check phon self.env[pattern.myid].rollback() - if not phon_ok: - errors.append(error.ErrorBadRhymeSound(self.env[pattern.myid], - self.env[pattern.myid].new_rhyme)) - if not eye_ok: - errors.append(error.ErrorBadRhymeEye(self.env[pattern.myid], - self.env[pattern.myid].new_rhyme)) + rhyme_failed = True + errors.append(error.ErrorBadRhymeSound(self.env[pattern.myid], + self.env[pattern.myid].new_rhyme)) # occurrences if self.options['check_occurrences']: @@ -155,6 +155,19 @@ class Template: v.phon = self.env[pattern.myid].phon v.parse() + + # now that we have parsed, adjust rhyme to reflect last word length + # and check eye + if not rhyme_failed: + self.env[pattern.myid].adjustLastCount(v.lastCount()) + if not self.env[pattern.myid].satisfied_eye(): + old_phon = len(self.env[pattern.myid].phon) + self.env[pattern.myid].rollback() + errors.append(error.ErrorBadRhymeEye(self.env[pattern.myid], + self.env[pattern.myid].new_rhyme, old_phon)) + + rhyme_failed = False + errors = v.problems() + errors if ofile: diff --git a/verse.py b/verse.py @@ -468,6 +468,21 @@ class Verse: 'fem': '\\', # preceding word ends by a mute e } + def lastCount(self): + """return min number of syllables for last word""" + + tot = 0 + for c in self.chunks[::-1]: + if c['original'].endswith(' '): + if tot > 0: + break + if 'weights' in c.keys(): + tot += min(c['weights']) + if ' ' in c['original'].rstrip(): + if tot > 0: + break + return tot + def align2str(self, align): return ''.join([x['text'] for x in align]) diff --git a/views/about.html b/views/about.html @@ -185,6 +185,12 @@ options suivantes sont reconnues&nbsp;:</p> <dt>eye_tolerance_ok</dt> <dd>Autoriser les tolérances pour les rimes pour l'œil telles que "-é"/"-ai" (vrai par défaut).</dd> + <dt>poor_eye_required</dt> + <dd>Imposer des rimes pour l'œil d'au moins 2 caractères dans le cas d'une + rime pauvre (un seul phonème) qui ne comporte pas de mots monosyllabiques + (vrai par défaut, n'a de sens que pour les rimes classiques).</dd> + <dt>poor_eye_supposed_ok</dt> + <dd>Autoriser la rime pour l'œil pour poor_eye_required (faux par défaut).</dd> </dl> <p>Désolé si tout cela semble un peu obscur. Vous pouvez regarder les modèles @@ -403,6 +409,13 @@ form option:value and separated by spaces, with the following possibilities:</p> <dt>eye_tolerance_ok</dt> <dd>Allow tolerances for <em>rimes pour l'œil</em> such as "-é"/"-ai" (default is true).</dd> + <dt>poor_eye_required</dt> + <dd>Require at least 2 characters in <em>rimes pour l'œil</em> when the sound + rhyme is poor (only one phoneme) and there are no monosyllabic words in the + rhyme (default is true, only applies to classical rhymes).</dd> + <dt>poor_eye_supposed_ok</dt> + <dd>Allow <em>rime pour l'œil</em> for poor_eye_required (default is + false).</dd> </dl> <p>Sorry if all of this is a bit obscure. You can have a look to the predefined