commit 694a57de12225d9db397683dc2ee676cd7931bb5
parent c225836d197489783f0903de0afad06695bf4e6b
Author: Antoine Amarilli <a3nm@a3nm.net>
Date: Fri, 20 Dec 2013 20:12:30 +0100
Merge branch 'master' of gitorious.org:plint/plint
Conflicts:
common.py
Diffstat:
17 files changed, 461 insertions(+), 253 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -9,7 +9,6 @@ irc/*
occurrences
old/*
*.pyc
-test/*
Lexique371/
contexts/
contexts_old/
diff --git a/TODO b/TODO
@@ -1,12 +1,12 @@
-"com-meuh L" ?
-highlight possible hemistiche positions where an hemistiche could have been placed
-faire rimer "I R C" avec "déplacé"
-pourquoi dans "A3nm" 'A' est interdit ?
-pourquoi dans "Nous médititions en cœur sur son déroulement" un "-ment" muet est accepté ?
-belle gestion des erreurs en html
-other examples of "mentions", "notions" ? ...
+- 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
+- produce error reports first to structured and then to text and json
+- and then from json to html
-handle "' " and "` "
+== Web ==
+
+- on web frontend, selecting a radio puts relevant template in the textarea
== IRC ==
@@ -18,13 +18,14 @@ handle "' " and "` "
== Alignment ==
-- improve rhyme error reporting
+- improve eye rhyme error reporting
== Rules ==
- is "[^gq]ue [consonne]" or "[^gq]ues" ok ?
- should we forbid "[^gq]uent", "ient", "éent" ?
- 3-syllable vowel clusters?
+- other examples of "mentions", "notions" ? ...
== Existing errors ==
diff --git a/common.py b/common.py
@@ -6,7 +6,7 @@ import re
vowels = 'aeiouyϾ'
consonants = "bcçdfghjklmnpqrstvwxzñĝ'"
-apostrophes = "'’"
+apostrophes = "'’`"
legal = vowels + consonants + ' -'
# a variant of x-sampa such that all french phonemes are one-character
@@ -83,10 +83,14 @@ def is_consonants(chunk):
return False
return True
-def normalize(text, downcase=True, rm_all=False, rm_apostrophe=False):
+def normalize(text, downcase=True, rm_all=False, rm_apostrophe=False, strip=True):
"""Normalize text, ie. lowercase, no useless punctuation or whitespace"""
- return norm_spaces(rm_punct(text.lower() if downcase else text,
- rm_all=rm_all, rm_apostrophe=rm_apostrophe)).rstrip().lstrip()
+ res = norm_spaces(rm_punct(text.lower() if downcase else text,
+ rm_all=rm_all, rm_apostrophe=rm_apostrophe))
+ if strip:
+ return res.rstrip().lstrip()
+ else:
+ return res
def subst(string, subs):
if len(subs) == 0:
diff --git a/error.py b/error.py
@@ -29,6 +29,8 @@ class ErrorCollection:
'error': lambda x, y: ErrorCollection.keys.get(x, '') *
len(chunk['original'])}
def render(chunk, key):
+ if key == 'error' and chunk.get('error', '') == 'illegal':
+ return chunk['illegal_str']
return (formatters.get(key, lambda x, y: str(x)))(chunk.get(key, ""), chunk)
lines = {}
for key in keys:
@@ -90,10 +92,7 @@ class ErrorBadRhyme:
self.inferred = inferred
def report(self, pattern):
- # TODO indicate eye rhyme since this is also important
- # TODO don't indicate more than the minimal required rhyme (in length and
- # present of a vowel phoneme)
- return (_("%s for type %s (expected \"%s\", inferred \"%s\")")
+ return (_("%s for type %s (expected %s, inferred %s)")
% (self.kind, self.get_id(pattern), self.fmt(self.expected),
self.fmt(self.inferred)))
@@ -106,30 +105,31 @@ class ErrorBadRhymeGenre(ErrorBadRhyme):
result = _(' or ').join(list(l))
if result == '':
result = "?"
- return result
+ return "\"" + result + "\""
def get_id(self, pattern):
return pattern.femid
-class ErrorBadRhymeSound(ErrorBadRhyme):
+class ErrorBadRhymeObject(ErrorBadRhyme):
+ def get_id(self, pattern):
+ return pattern.myid
+
+class ErrorBadRhymeSound(ErrorBadRhymeObject):
@property
def kind(self):
- return _("Bad rhyme")
+ return _("Bad rhyme sound")
def fmt(self, l):
- pron = l.phon
- ok = []
- if len(pron) > 0:
- ok.append("")
- return ("\"" + '/'.join(list(set([common.to_xsampa(x[-4:]) for x in pron])))
- + "\"" + _(", ending: \"") + l.eye + "\"")
+ return '/'.join("\"" + common.to_xsampa(x) + "\"" for x in
+ l.sufficient_phon())
- def get_id(self, pattern):
- return pattern.myid
+class ErrorBadRhymeEye(ErrorBadRhymeObject):
+ @property
+ def kind(self):
+ return _("Bad rhyme ending")
- def report(self, pattern):
- return (_("%s for type %s (expected %s)")
- % (self.kind, pattern.myid, self.fmt(self.expected)))
+ def fmt(self, l):
+ return "\"-" + l.sufficient_eye() + "\""
class ErrorBadMetric:
def report(self, pattern):
diff --git a/plint_irc.py b/plint_irc.py
@@ -61,6 +61,10 @@ def manage(line, descriptor=sys.stdout):
return False
if not leading_cap(text):
return False
+ if (not (text.rstrip().endswith("...") or text.rstrip().endswith("…")
+ or text.lstrip().endswith("...") or text.lstrip().endswith("…")) and
+ len(text) < 13):
+ return False # too short
if (text.rstrip().endswith("...") or
text.rstrip().endswith("…")):
# it might be a call
diff --git a/res/messages_fr.po b/res/messages_fr.po
@@ -1,59 +1,57 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR ORGANIZATION
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# plint localization to French
+# Copyright (C) 2013 Antoine Amarilli
+# Antoine Amarilli <a3nm AT a3nm DOT net>, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: plint\n"
-"POT-Creation-Date: 2013-05-09 01:25+CEST\n"
-"PO-Revision-Date: 2013-05-09 01:26+0100\n"
+"POT-Creation-Date: 2013-10-31 23:32+CET\n"
+"PO-Revision-Date: 2013-10-31 23:35+0100\n"
"Last-Translator: Antoine Amarilli <a3nm@a3nm.net>\n"
"Language-Team: \n"
+"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 1.5.4\n"
-#: error.py:67
+#: error.py:69
msgid " (see '%s' above)"
msgstr " (voir '%s' ci-dessus)"
-#: error.py:72
+#: error.py:74
msgid "Illegal characters"
msgstr "Caractères interdits"
-#: error.py:78
+#: error.py:80
msgid "Illegal ambiguous pattern"
msgstr "Motif ambigu interdit"
-#: error.py:84
+#: error.py:86
msgid "Illegal hiatus"
msgstr "Hiatus interdit"
-#: error.py:96
-msgid "%s for type %s (expected \"%s\", inferred \"%s\")"
-msgstr "%s pour le type %s (attendu : \"%s\", lu : \"%s\")"
+#: error.py:95
+msgid "%s for type %s (expected %s, inferred %s)"
+msgstr "%s pour le type %s (attendu : %s, lu : %s)"
-#: error.py:103
+#: error.py:102
msgid "Bad rhyme genre"
msgstr "Mauvais genre de rime"
-#: error.py:106
+#: error.py:105
msgid " or "
msgstr " ou "
-#: error.py:117
-msgid "Bad rhyme"
+#: error.py:120
+#, fuzzy
+msgid "Bad rhyme sound"
msgstr "Mauvaise rime"
-#: error.py:125
-msgid ", ending: \""
-msgstr ", fin: \""
-
-#: error.py:131
-msgid "%s for type %s (expected %s)"
-msgstr "%s pour le type %s (attendu : %s)"
+#: error.py:129
+msgid "Bad rhyme ending"
+msgstr "Mauvaise rime pour l'œil"
#: error.py:136
msgid "Illegal metric: expected %d syllable%s%s"
@@ -85,30 +83,36 @@ msgstr ""
"Vérifie l'entrée standard suivant MODÈLE, signale les erreurs sur la sortie "
"standard"
-#: template.py:28
+#: template.py:29
msgid "Metric description should only contain positive integers"
msgstr "La métrique ne doit contenir que des entiers strictement positifs"
-#: template.py:30
+#: template.py:31
msgid "Metric length limit exceeded"
msgstr "La longueur de la métrique est trop grande"
-#: template.py:77
+#: template.py:78
msgid "Bad value for global option %s"
msgstr "Mauvaise valeur pour l'option globale %s"
-#: template.py:87
+#: template.py:88
msgid "Unknown global option"
msgstr "Option globale inconnue"
-#: template.py:102
+#: template.py:103
msgid "Template is empty"
msgstr "Modèle vide"
-#: template.py:270
+#: template.py:276
msgid "Bad value in global option"
msgstr "Mauvaise valeur pour l'option globale %s"
+#~ msgid ", ending: \""
+#~ msgstr ", fin: \""
+
+#~ msgid "%s for type %s (expected %s)"
+#~ msgstr "%s pour le type %s (attendu : %s)"
+
#~ msgid "hiatus"
#~ msgstr "hiatus"
diff --git a/rhyme.py b/rhyme.py
@@ -14,6 +14,7 @@ NBEST = 5
# phonetic vowels
vowel = list("Eeaio592O#@y%u()$")
+# use for supposed liaison both in phon and eye
liaison = {
'c': 'k',
'd': 't',
@@ -53,7 +54,7 @@ class Constraint:
if not c:
return
self.phon = self.mmax(self.phon, c.phon)
- self.eye = self.classical or c.classical
+ self.classical = self.classical or c.classical
class Rhyme:
def apply_mergers(self, phon):
@@ -61,19 +62,44 @@ class Rhyme:
else x) for x in phon])
def supposed_liaison(self, x):
- if x[-1] in liaison.keys():
+ if x[-1] in liaison.keys() and self.options['eye_supposed_ok']:
return x + liaison[x[-1]]
return x
- def __init__(self, line, constraint, mergers=[], normande_ok=True):
+ def __init__(self, line, constraint, mergers, options, phon=None):
self.constraint = constraint
self.mergers = {}
- self.normande_ok = normande_ok
+ self.options = options
for phon_set in mergers:
- for phon in phon_set[1:]:
- self.mergers[phon] = phon_set[0]
- self.phon = set([self.apply_mergers(x) for x in self.lookup(line)])
- self.eye = self.supposed_liaison(consonant_suffix(line))
+ for pho in phon_set[1:]:
+ self.mergers[pho] = phon_set[0]
+ if not phon:
+ 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.old_phon = None
+ self.old_eye = None
+ self.new_rhyme = None
+
+ def rollback(self):
+ self.phon = self.old_phon
+ self.eye = self.old_eye
+
+ def sufficient_phon(self):
+ # return the shortest accepted rhymes among old_phon
+ ok = set()
+ for p in self.phon:
+ slen = len(p)
+ for i in range(len(p)):
+ if p[-(i+1)] in vowel:
+ slen = i+1
+ break
+ slen = max(slen, self.constraint.phon)
+ ok.add(p[-slen:])
+ return ok
+
+ def sufficient_eye(self):
+ return self.eye[-1]
def match(self, phon, eye):
"""limit our phon and eye to those which match phon and eye and which
@@ -84,7 +110,6 @@ class Rhyme:
val = phon_rhyme(x, y)
if val >= self.constraint.phon and self.constraint.phon >= 0:
new_phon.add(x[-val:])
- val = assonance_rhyme(x, y)
self.phon = new_phon
if self.eye:
val = eye_rhyme(self.eye, eye)
@@ -95,31 +120,45 @@ class Rhyme:
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.constraint.restrict(r.constraint)
+ self.new_rhyme = r
self.match(set([self.apply_mergers(x) for x in r.phon]),
- self.supposed_liaison(consonant_suffix(r.eye)))
+ self.supposed_liaison(self.consonant_suffix(r.eye)))
+
+ def consonant_suffix(self, s):
+ if not self.options['eye_tolerance_ok']:
+ return s
+ for k in tolerance.keys():
+ if s.endswith(k):
+ return s[:-(len(k))] + tolerance[k]
+ return s
def feed(self, line, constraint=None):
"""extend us with a line and a constraint"""
- return self.restrict(Rhyme(line, constraint, self.mergers))
+ return self.restrict(Rhyme(line, constraint, self.mergers, self.options))
+
+ def satisfied_phon(self):
+ return len(self.phon) >= self.constraint.phon
+
+ def satisfied_eye(self):
+ return (len(self.eye) > 0 or not self.constraint.classical)
def satisfied(self):
- return (len(self.phon) >= self.constraint.phon
- and len(self.eye) >= self.constraint.eye
- and (len(self.eye) > 0 or not self.constraint.classical))
+ return self.satisfied_phon() and self.satisfied_eye()
def pprint(self):
pprint(self.phon)
- def lookup(self, s):
- """lookup the pronunciation of s, adding rime normande kludges and liaisons"""
- result = raw_lookup(s)
- if self.normande_ok and (s.endswith('er') or s.endswith('ers')):
- result.add("ER")
- # TODO better here
+ def adjust(self, result, s):
+ """add liason kludges"""
result2 = copy.deepcopy(result)
- # the case 'ent' would lead to trouble for gender
- if self.constraint.classical:
+ # adjust for tolerance with classical rhymes
+ # e.g. "vautours"/"ours", "estomac"/"Sidrac"
+ if self.options['phon_supposed_ok']:
+ # the case 'ent' would lead to trouble for gender
if s[-1] in liaison.keys() and not s.endswith('ent'):
for r in result2:
result.add(r + liaison[s[-1]])
@@ -127,6 +166,12 @@ class Rhyme:
result.add(r + 's')
return result
+ def lookup(self, s):
+ """lookup the pronunciation of s, adding rime normande kludges"""
+ result = raw_lookup(s)
+ if self.options['normande_ok'] and (s.endswith('er') or s.endswith('ers')):
+ result.add("ER")
+ return self.adjust(result, s)
def suffix(x, y):
"""length of the longest common suffix of x and y"""
@@ -148,12 +193,6 @@ def phon_rhyme(x, y):
return nphon
return 0
-def strip_consonants(x):
- return str([a for a in x if a in vowel or a == 'j'])
-
-def assonance_rhyme(x, y):
- return phon_rhyme(strip_consonants(x), strip_consonants(y))
-
def eye_rhyme(x, y):
"""value of x and y as an eye rhyme"""
return suffix(x, y)
@@ -166,16 +205,6 @@ def concat_couples(a, b):
s.add(x + y)
return s
-def consonant_suffix(s):
- for k in tolerance.keys():
- if s.endswith(k):
- s = s[:-(len(k))] + tolerance[k]
- for i in range(len(s)):
- if not s[-(i+1)] in consonants:
- break
- result = s[-(i):]
- return result
-
def raw_lookup(s):
# kludge: take the last three words and concatenate them to take short words
# into account
@@ -197,7 +226,7 @@ if __name__ == '__main__':
if len(line) < 1:
continue
constraint = Constraint(True, 1)
- rhyme = Rhyme(line[0], constraint, self.mergers, self.normande_ok)
+ rhyme = Rhyme(line[0], constraint, self.mergers, self.options)
for x in line[1:]:
rhyme.feed(x)
rhyme.pprint()
diff --git a/static/main.css b/static/main.css
@@ -48,6 +48,7 @@ header {
#custom_template {
height: 8em;
+ display: none;
}
body {
diff --git a/static/script.js b/static/script.js
@@ -0,0 +1,7 @@
+function showCustom(a) {
+ if (a) {
+ document.getElementById("custom_template").style.display = "block";
+ } else {
+ document.getElementById("custom_template").style.display = "none";
+ }
+}
diff --git a/template.py b/template.py
@@ -37,19 +37,38 @@ class Pattern:
self.length = self.hemistiches.pop()
class Template:
+ option_aliases = {
+ 'fusionner': 'merge',
+ 'ambiguous_ok': 'forbidden_ok',
+ 'ambigu_ok': 'forbidden_ok',
+ 'dierese': 'diaeresis',
+ 'verifie_fin_hemistiche': 'check_end_hemistiche',
+ 'verifie_occurrences': 'check_occurrences',
+ 'repetition_ok': 'repeat_ok',
+ 'incomplet_ok': 'incomplete_ok',
+ 'phon_supposee_ok': 'phon_supposed_ok',
+ 'oeil_supposee_ok': 'eye_supposed_ok',
+ 'oeil_tolerance_ok': 'eye_tolerance_ok'
+ }
+
def __init__(self, string=None):
self.template = []
self.pattern_line_no = 0
- self.forbidden_ok = False
- self.hiatus_ok = False
- self.normande_ok = True
- self.repeat_ok = True
- self.overflowed = False
- self.incomplete_ok = True
- self.check_end_hemistiche = True
- self.check_occurrences = True
- self.diaeresis = "classical"
+ self.options = {
+ 'forbidden_ok': False,
+ 'hiatus_ok': False,
+ 'normande_ok': True,
+ 'eye_supposed_ok': True,
+ 'phon_supposed_ok': True,
+ 'eye_tolerance_ok': True,
+ 'repeat_ok': True,
+ 'incomplete_ok': True,
+ 'check_end_hemistiche': True,
+ 'check_occurrences': True,
+ 'diaeresis': "classical"
+ }
self.mergers = []
+ self.overflowed = False
if string != None:
self.load(string)
self.line_no = 0
@@ -62,28 +81,18 @@ class Template:
def read_option(self, x):
key, value = x.split(':')
- if key in ["merge", "fusionner"]:
+ if key in self.option_aliases.keys():
+ key = self.option_aliases[key]
+ if key == 'merge':
self.mergers.append(value)
- elif key in ["forbidden_ok", "ambiguous_ok", "ambigu_ok"]:
- self.forbidden_ok = str2bool(value)
- elif key in ["hiatus_ok"]:
- self.hiatus_ok = str2bool(value)
- elif key in ["normande_ok"]:
- self.normande_ok = str2bool(value)
- elif key in ["diaeresis", "dierese"]:
+ elif key == 'diaeresis':
if value == "classique":
value = "classical"
- self.diaeresis = value
if value not in ["permissive", "classical"]:
raise error.TemplateLoadError(_("Bad value for global option %s") % key)
- elif key in ["check_end_hemistiche", "verifie_fin_hemistiche"]:
- self.check_end_hemistiche = str2bool(value)
- elif key in ["check_occurrences", "verifie_occurrences"]:
- self.check_occurrences = str2bool(value)
- elif key in ["repeat_ok"]:
- self.repeat_ok = str2bool(value)
- elif key in ["incomplete_ok"]:
- self.incomplete_ok = str2bool(value)
+ self.options['diaeresis'] = value
+ elif key in self.options.keys():
+ self.options[key] = str2bool(value)
else:
raise error.TemplateLoadError(_("Unknown global option"))
@@ -111,12 +120,11 @@ class Template:
pattern = self.get()
line_with_case = normalize(line, downcase=False)
- line_normalize = normalize(line)
v = Verse(line, self, pattern)
if last:
- if was_incomplete and not self.incomplete_ok and not self.overflowed:
+ if was_incomplete and not self.options['incomplete_ok'] and not self.overflowed:
return [error.ErrorIncompleteTemplate()], pattern, v
return [], pattern, v
@@ -126,32 +134,25 @@ class Template:
# rhymes
if pattern.myid not in self.env.keys():
# initialize the rhyme
- self.env[pattern.myid] = rhyme.Rhyme(line_normalize, pattern.constraint,
- self.mergers, self.normande_ok)
+ self.env[pattern.myid] = rhyme.Rhyme(v.normalized, pattern.constraint,
+ self.mergers, self.options)
else:
# update the rhyme
- old_p = self.env[pattern.myid].phon
- old_e = self.env[pattern.myid].eye
- self.env[pattern.myid].feed(line_normalize, pattern.constraint)
- # no more possible rhymes, something went wrong
+ self.env[pattern.myid].feed(v.normalized, pattern.constraint)
if not self.env[pattern.myid].satisfied():
- self.env[pattern.myid].phon = old_p
- self.env[pattern.myid].eye = old_e
- errors.append(error.ErrorBadRhymeSound(self.env[pattern.myid], None))
-
- errors += v.problems()
-
- if ofile:
- possible = v.possible
- if len(possible) == 1:
- for i, p in enumerate(possible[0]):
- if ('weight' in p.keys() and len(p['weights']) > 1
- and p['weight'] > 0):
- print(str(p['weight']) + ' '
- + ' '.join(make_query(possible[0], i)), file=ofile)
+ # no more possible rhymes, something went wrong
+ phon_ok = self.env[pattern.myid].satisfied_phon()
+ eye_ok = self.env[pattern.myid].satisfied_eye()
+ 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))
# occurrences
- if self.check_occurrences:
+ if self.options['check_occurrences']:
if pattern.myid not in self.occenv.keys():
self.occenv[pattern.myid] = {}
last_word = re.split(r'[- ]', line_with_case)[-1]
@@ -159,9 +160,22 @@ class Template:
self.occenv[pattern.myid][last_word] = 0
self.occenv[pattern.myid][last_word] += 1
if self.occenv[pattern.myid][last_word] > nature_count(last_word):
- errors.append(error.ErrorMultipleWordOccurrence(last_word,
+ errors.insert(0, error.ErrorMultipleWordOccurrence(last_word,
self.occenv[pattern.myid][last_word]))
+ v.phon = self.env[pattern.myid].phon
+ v.parse()
+ errors = v.problems() + errors
+
+ if ofile:
+ possible = v.possible
+ if len(possible) == 1:
+ for i, p in enumerate(possible[0]):
+ if ('weight' in p.keys() and len(p['weights']) > 1
+ and p['weight'] > 0):
+ print(str(p['weight']) + ' '
+ + ' '.join(make_query(possible[0], i)), file=ofile)
+
# rhyme genres
# inequality constraint
# TODO this is simplistic and order-dependent
@@ -178,7 +192,7 @@ class Template:
x = set(['M', 'F'])
self.femenv[pattern.femid] = x
old = list(self.femenv[pattern.femid])
- new = v.genders(self.env[pattern.myid].phon)
+ new = v.genders()
self.femenv[pattern.femid] &= set(new)
if len(self.femenv[pattern.femid]) == 0:
errors.append(error.ErrorBadRhymeGenre(old, new))
@@ -233,7 +247,7 @@ class Template:
self.old_femenv = copy.deepcopy(self.femenv)
self.old_occenv = copy.deepcopy(self.occenv)
if self.beyond:
- if not self.repeat_ok:
+ if not self.options['repeat_ok']:
self.overflowed = True
self.reset_state()
result = self.template[self.position]
diff --git a/test/letters b/test/letters
@@ -0,0 +1,4 @@
+b c q d e f r s t w
+p t q r s t u v x b c c
+patata t t x r x x une hache
+patata f tata tatata tatar h
diff --git a/test/letters.tpl b/test/letters.tpl
@@ -0,0 +1,4 @@
+6/6 A !X
+6/6 A !X
+6/6 B !x
+6/6 B !x
diff --git a/verse.py b/verse.py
@@ -8,6 +8,27 @@ import haspirater
import error
from pprint import pprint
+# the writing is designed to make frhyme succeed
+# end vowels will be elided
+# missing letters have a default case
+letters = {
+ 'f': 'effe',
+ 'h': 'ache',
+ 'j': 'gi',
+ 'k': 'ka',
+ 'l': 'elle',
+ 'm': 'aime',
+ 'n': 'aine',
+ 'q': 'cu',
+ 'r': 'ère',
+ 's': 'esse',
+ 'w': 'doublevé',
+ 'x': 'ixe',
+ 'z': 'zaide'
+}
+
+
+
class Verse:
def elision(self, word):
if (word.startswith('y') and not word == 'y' and not word.startswith("yp") and
@@ -48,9 +69,17 @@ class Verse:
def line(self):
return ''.join(x['original'] for x in self.chunks)
+ @property
+ def normalized(self):
+ return ''.join(normalize(x['original'], strip=False)
+ if 'text_pron' not in x.keys() else x['text']
+ for x in self.chunks).lstrip().rstrip()
+
def __init__(self, line, template, pattern):
self.template = template
self.pattern = pattern
+ # will be updated later, used in parse and feminine
+ self.phon = None
whitespace_regexp = re.compile("(\s*)")
ys_regexp = re.compile("(\s*)")
@@ -82,9 +111,15 @@ class Verse:
# check forbidden characters
for w in self.chunks:
for y in w:
+ es = ""
for x in y['text']:
if not common.rm_punct(strip_accents_one(x)[0].lower()) in common.legal:
+ es += 'I'
y['error'] = "illegal"
+ else:
+ es += ' '
+ if 'error' in y.keys() and y['error'] == "illegal":
+ y['illegal_str'] = es
# gu- and qu- simplifications
for w in self.chunks:
@@ -116,23 +151,41 @@ class Verse:
if len(w) == 1 and is_consonants(w[0]['text']):
new_chunks = []
for j, x in enumerate(w[0]['text']):
- if (x == 'w'):
- nc = "doublevé"
- else:
- nc = x + "a"
- new_chunks += re.split(consonants_regexp, nc)
- new_chunks = [x for x in new_chunks if len(x) > 0]
+ try:
+ nc = letters[x]
+ # hack: the final 'e's in letters are just to help pronunciation
+ # inference and are only needed at end of word, otherwise they will
+ # mess syllable count up
+ if j < len(w[0]['text']) - 1 and nc[-1] == 'e':
+ nc = nc[:-1]
+ except KeyError:
+ nc = x + 'é'
+ new_chunks += [(j, x) for x in re.split(consonants_regexp, nc)]
+ new_chunks = [x for x in new_chunks if len(x[1]) > 0]
new_word = []
- for j, x in enumerate(new_chunks):
- lindex = int(j*len(w[0]['original'])/len(w[0]['text']))
- rindex = int((j+1)*len(w[0]['original'])/len(w[0]['text']))
- part = w[0]['original'][lindex:rindex]
- new_word.append({'original': part, 'text': x})
+ last_opos = -1
+ for j, (opos, x) in enumerate(new_chunks):
+ part = ""
+ if j == len(new_chunks) - 1:
+ # don't miss final spaces
+ part = w[0]['original'][last_opos+1:]
+ elif last_opos < opos:
+ part = w[0]['original'][last_opos+1:opos+1]
+ last_opos = opos
+ # allow or forbid elision because of possible ending '-e' before
+ # forbid hiatus both for this and for preceding
+ # instruct that we must use text for the pronunciation
+ new_word.append({'original': part, 'text': x, 'text_pron': True,
+ 'elision': [False, True], 'no_hiatus': True})
self.chunks[i] = new_word
+ # the last one is also elidable
+ if self.chunks[i][-1]['text'] == 'e':
+ self.chunks[i][-1]['elidable'] = [True]
# vowel elision problems
for w in self.chunks:
- w[0]['elision'] = self.elision(''.join(x['text'] for x in w))
+ if 'elision' not in w[0].keys():
+ w[0]['elision'] = self.elision(''.join(x['text'] for x in w))
# case of 'y'
ys_regexp = re.compile("(y*)")
@@ -173,7 +226,8 @@ class Verse:
continue
if sum([1 for chunk in w if is_vowels(chunk['text'])]) <= 1:
continue
- w[-1]['elidable'] = self.chunks[i+1][0]['elision']
+ if 'elidable' not in w[-1].keys():
+ w[-1]['elidable'] = self.chunks[i+1][0]['elision']
# annotate hiatus and ambiguities
ambiguous_potential = ["ie", "ée"]
@@ -188,11 +242,13 @@ class Verse:
w[-1]['error'] = "ambiguous"
self.chunks[i+1][0]['error'] = "ambiguous"
elif is_vowels(w[-1]['text']) and not w[-1]['text'].endswith('e'):
- if is_vowels(self.chunks[i+1][0]['text']):
+ if (is_vowels(self.chunks[i+1][0]['text']) and 'no_hiatus' not in
+ self.chunks[i+1][0].keys()):
if ''.join(x['text'] for x in w) not in no_hiatus:
if ''.join(x['text'] for x in self.chunks[i+1]) not in no_hiatus:
- w[-1]['error'] = "hiatus"
- self.chunks[i+1][0]['error'] = "hiatus"
+ if 'no_hiatus' not in w[-1].keys():
+ w[-1]['error'] = "hiatus"
+ self.chunks[i+1][0]['error'] = "hiatus"
# annotate word ends
for w in self.chunks[:-1]:
@@ -201,6 +257,7 @@ class Verse:
# collapse words
self.chunks = sum(self.chunks, [])
+ def parse(self):
# annotate weights
for i, chunk in enumerate(self.chunks):
if (not is_vowels(self.chunks[i]['text'])):
@@ -217,9 +274,9 @@ class Verse:
return '-' in chunk['text'] or 'wordend' in chunk
def possible_weights(self, pos):
- if self.template.diaeresis == "classical":
+ if self.template.options['diaeresis'] == "classical":
return vowels.possible_weights_ctx(self.chunks, pos)
- elif self.template.diaeresis == "permissive":
+ elif self.template.options['diaeresis'] == "permissive":
return vowels.possible_weights_approx(self.chunks[pos]['text'])
def possible_weights_context(self, pos):
@@ -230,14 +287,25 @@ class Verse:
and not (pos <= 1 or self.contains_break(self.chunks[pos-2]))):
# special case for verse endings, which can get elided (or not)
# but we don't elide lone syllables ("prends-le", etc.)
+
+
if pos == len(self.chunks) - 1:
return [0] # ending 'e' is elided
if self.chunks[pos+1]['text'] == 's':
return [0] # ending 'es' is elided
if self.chunks[pos+1]['text'] == 'nt':
- # ending 'ent' is sometimes elided
+ # ending 'ent' is sometimes elided, try to use pronunciation
# actually, this will have an influence on the rhyme's gender
- return [0, 1]
+ # see feminine
+ possible = []
+ if len(self.phon) == 0:
+ return [0, 1] # do something reasonable without pron
+ for possible_phon in self.phon:
+ if possible_phon.endswith(')') or possible_phon.endswith('#'):
+ possible.append(1)
+ else:
+ possible.append(0)
+ return possible
return self.possible_weights(pos)
if (pos == len(self.chunks) - 1 and self.chunks[pos]['text'] == 'e' and
pos > 0 and (self.chunks[pos-1]['text'].endswith('-c') or
@@ -256,7 +324,7 @@ class Verse:
return [0 if x else 1 for x in self.chunks[pos]['elidable']]
return self.possible_weights(pos)
- def feminine(self, align, phon):
+ def feminine(self, align):
for a in sure_end_fem:
if self.text.endswith(a):
# check that this isn't a one-syllabe wourd
@@ -277,7 +345,7 @@ class Verse:
possible = []
# now, we must check pronunciation?
# "tient" vs. "lient" for instance, "excellent"...
- for possible_phon in phon:
+ for possible_phon in self.phon:
if possible_phon.endswith(')') or possible_phon.endswith('#'):
possible.append('M')
else:
@@ -303,7 +371,8 @@ class Verse:
next_hemistiches = hemistiches
if (len(hemistiches) > 0 and count + weight == hemistiches[0] and
is_vowels(chunk['text']) and (chunk['hemis'] == "ok" or not
- self.template.check_end_hemistiche and chunk['hemis'] != "cut")):
+ self.template.options['check_end_hemistiche'] and
+ chunk['hemis'] != "cut")):
# we hemistiche here
next_hemistiches = next_hemistiches[1:]
current = dict(self.chunks[pos])
@@ -347,30 +416,31 @@ class Verse:
def problems(self):
result = []
errors = set()
+ if len(self.possible) == 0:
+ result.append(error.ErrorBadMetric())
for c in self.chunks:
if 'error' in c:
- if c['error'] == "ambiguous" and not self.template.forbidden_ok:
+ if (c['error'] == "ambiguous" and not
+ self.template.options['forbidden_ok']):
errors.add(error.ErrorForbiddenPattern)
- if c['error'] == "hiatus" and not self.template.hiatus_ok:
+ if c['error'] == "hiatus" and not self.template.options['hiatus_ok']:
errors.add(error.ErrorHiatus)
if c['error'] == "illegal":
errors.add(error.ErrorBadCharacters)
for k in errors:
result.append(k())
- if len(self.possible) == 0:
- result.append(error.ErrorBadMetric())
return result
def valid(self):
return len(self.problems()) == 0
- def genders(self, phon):
+ def genders(self):
result = set()
for p in self.possible:
- result.update(set(self.feminine(p, phon)))
+ result.update(set(self.feminine(p)))
if len(self.possible) == 0:
# try to infer gender even when metric is wrong
- result.update(set(self.feminine(None, phon)))
+ result.update(set(self.feminine(None)))
return result
diff --git a/versetest.py b/versetest.py
@@ -9,38 +9,45 @@ class SanityCheck(unittest.TestCase):
def testSimple(self):
text = "Hello World!! This is a test"
v = verse.Verse(text, template.Template(), template.Pattern("12"))
+ v.parse()
self.assertEqual(text, v.line)
def testComplex(self):
text = "Aye AYAYE aye gue que geque AYAYAY a prt sncf bbbéé"
v = verse.Verse(text, template.Template(), template.Pattern("12"))
+ v.parse()
self.assertEqual(text, v.line)
def testLeadingSpace(self):
text = " a"
v = verse.Verse(text, template.Template(), template.Pattern("12"))
+ v.parse()
self.assertEqual(text, v.line)
class Eliminate(unittest.TestCase):
def testEliminateOneGue(self):
text = "gue"
v = verse.Verse(text, template.Template(), template.Pattern("12"))
+ v.parse()
c = ''.join([x['text'] for x in v.chunks])
self.assertFalse("gue" in c)
def testEliminateGue(self):
text = "gue gue GUE ogues longuement la guerre"
v = verse.Verse(text, template.Template(), template.Pattern("12"))
+ v.parse()
c = ''.join([x['text'] for x in v.chunks])
self.assertFalse("gue" in c)
class BadChars(unittest.TestCase):
def testBadAlone(self):
v = verse.Verse("42", template.Template(), template.Pattern("12"))
+ v.parse()
self.assertFalse(v.valid())
def testBadAndGood(self):
v = verse.Verse("bla h42 blah ", template.Template(), template.Pattern("12"))
+ v.parse()
self.assertFalse(v.valid())
@@ -56,6 +63,7 @@ class BadChars(unittest.TestCase):
class Counts(unittest.TestCase):
def runCount(self, text, limit=12):
v = verse.Verse(text, template.Template(), template.Pattern(str(limit)))
+ v.parse()
return v.possible
def getWeight(self, align):
@@ -129,7 +137,6 @@ class RealCounts(Counts):
class BadCounts(Counts):
def testBad(self):
f = self.runCount("Cela cela", limit=5)
- pprint(f)
self.assertEqual(0, len(f))
class PoemCounts(Counts):
diff --git a/views/about.html b/views/about.html
@@ -26,10 +26,11 @@ aucun des modèles ne vous convient, vous pouvez <a href="#template">écrire le
vôtre</a>.</p>
<h2>Qu'est-ce qui est vérifié par plint ?</h2>
-<p>Ces explications simplifiées ne sont pas exhaustives. Pour une description
-exacte, se reporter au code source.</p>
+<p>Les erreurs suivantes sont signalées par défaut. Des options de configuration
+dans le modèle permettent de rendre plint plus tolérant sur la plupart de ces
+erreurs : cela est documenté <a href="#template">plus bas</a>.</p>
<dl>
- <dt>Nombre de syllabes</dt>
+ <dt>Métrique</dt>
<dd>Le nombre de syllabes par vers est habituellement fixé (12 pour les
alexandrins, par exemple). Les syllabes sont comptées en scandant sans
élision des <em>e</em> muets sauf quand un mot se termine par un <em>e</em>
@@ -37,30 +38,28 @@ exacte, se reporter au code source.</p>
(ou si c'est le dernier mot du vers). Le nombre de syllabes n'est <em>pas</em>
ce que vous obtenez en lisant le poème comme vous parlez tous les jours.
Certains groupes de voyelles peuvent prendre deux syllabes soit
- systématiquement (par exemple "Léon") ou par une <em>diérèse</em> facultative
- ou obligatoire ("passion") ; plint utilise une approximation des diérèses
- effectivement autorisées déduite à partir d'un corpus de textes classiques.
- Par ailleurs, certains motifs ambigus comme "ies" et "ées" sont
- interdits.</dd>
+ systématiquement (par exemple "Léon") soit par une <em>diérèse</em> facultative
+ ou obligatoire ("passion") ; plint utilise une approximation permissive
+ des diérèses effectivement autorisées déduite à partir d'un corpus de textes
+ classiques.</dd>
<dt>Hémistiche</dt>
<dd>Les alexandrins classiques sont divisés en deux <em>hémistiches</em> de
6 syllabes. La césure ne doit pas couper un mot et le premier hémistiche ne
doit pas se finir par un son faible (ie. une fin féminine non élidée).</dd>
- <dt>Rime.</dt>
- <dd>La contrainte la plus connue est que les vers doivent rimer. Les phonèmes
- communs dans une rime doivent inclure un son vocalique (par exemple "tâte" et
- "bête" ne riment pas parce que leur suffixe commun est [t] qui ne contient pas
- de son vocalique). Les rimes sont vérifiées par plint avec une approximation
- permissive de la prononciation de la fin des vers. Plint confond [ɛ] et [ɛː]
- ("mettre" et "maître") et [a] et [ɑ] ("patte" et "pâte") mais pas [ɛ̃] et [œ̃]
- ("brin" et "brun") ou [ɔ] et [o] ("cotte" et "côte"). Quand toutes les règles
- classiques sont suivies, certaines rimes comme la rime normande ou la rime
- avec liaison supposée sont acceptées mais un degré minimal de rime "pour
- l'œil" est exigé (ainsi "-é" et "-er" ne riment pas), plint essaie de suivre
- tout cela mais les détails sont complexes. On ne peut pas faire rimer un mot
- avec lui-même (mais on peut faire rimer un mot avec des homographes de nature
- grammaticale différente, ainsi le nombre d'occurrences d'un mot dans un motif
- de rimes est majoré par son nombre de natures grammaticales possibles).</dd>
+ <dt>Rime</dt>
+ <dd>La contrainte poétique la plus connue est que les vers doivent rimer. Les
+ phonèmes communs dans une rime doivent inclure un son vocalique (par exemple
+ "tâte" et "bête" ne riment pas parce que leur suffixe commun est [t] qui ne
+ contient pas de son vocalique). Les rimes sont vérifiées par plint avec une
+ approximation permissive de la prononciation de la fin des vers. Plint confond
+ [ɛ] et [ɛː] ("mettre" et "maître") et [a] et [ɑ] ("patte" et "pâte") mais pas
+ [ɛ̃] et [œ̃] ("brin" et "brun") ou [ɔ] et [o] ("cotte" et "côte"). Quand toutes
+ les règles classiques sont suivies, certaines rimes comme la rime normande ou
+ la rime avec liaison supposée sont acceptées.</dd>
+ <dt>Rime pour l'œil</dt>
+ <dd>En poésie classique, un degré minimal de rime "pour l'œil" est exigé
+ (ainsi "-é" et "-er" ne riment pas), avec une certaine tolérance (pour "-é" et
+ "-ai" notamment).</dd>
<dt>Genre des rimes.</dt>
<dd>En poésie classique, les rimes doivent être <em>féminines</em> ou
<em>masculines</em>. Une rime est féminine si elle se termine par un "e", "es"
@@ -68,18 +67,42 @@ exacte, se reporter au code source.</p>
faire rimer alternativement deux terminaisons féminines et deux terminaisons
masculines. plint vérifie cela, avec une approximation permissive pour
déterminer si une terminaison est féminine ou non.</dd>
+ <dt>Motifs ambigus</dt>
+ <dd>Certains motifs ambigus comme "-ies" et "-ées", ou "-ie" ou "-ée" suivis
+ d'une consonne ou d'un 'h' aspiré, sont interdits en poésie classique et
+ signalés.</dd>
+ <dt>Hiatus</dt>
+ <dd>Le hiatus (juxtaposition de sons vocaliques sans 'e' muet intercalaire)
+ est interdit en poésie classique et signalé.</dd>
+ <dt>Mots répétés</dt>
+ <dd>On ne doit pas faire rimer un mot avec lui-même (mais on peut faire rimer
+ un mot avec des homographes de nature grammaticale différente), ainsi, plint
+ signale une erreur si le nombre d'occurrences d'un mot connu dans un motif de
+ rimes est plus grand par son nombre de natures grammaticales possibles.</dd>
+ <dt>Caractères interdits</dt>
+ <dd>Les caractères que plint ne reconnaît pas (notamment les chiffres arabes)
+ sont signalés comme des erreurs, d'une part parce qu'ils sont sans doute
+ irréguliers en poésie classique, d'autre part parce que cela signifie sans
+ doute que plint va mal interpréter le vers.</dd>
+ <dt>Longueur</dt>
+ <dd>Pour certains types de poèmes (par exemple les sonnets), le nombre de vers
+ est fixé, et une erreur est levée si le modèle n'est pas complet ou s'il a été
+ répété.</dd>
</dl>
<h2>Qu'est-ce qui n'est pas vérifié par plint ?</h2>
<p>Le comptage des syllabes est effectué par une approximation permissive qui
-peut autoriser certaines diérèses ou synérèses non autorisées. Presque aucune
+peut autoriser certaines diérèses ou synérèses non autorisées. Aucune
vérification n'est faite pour s'assurer que la césure à l'hémistiche se fait à
une position grammaticale raisonnable. Les rimes sont calculées par une
-approximation permissive, et il en va de même pour les genres. Plus
-important : plint ne vérifie pas si votre poème a un sens, il impose
-uniquement des contraintes formelles. Par exemple, <q>Tatata tatati tatati
- tatata</q> est reconnu comme un alexandrin classique parfaitement valide.</p>
+approximation permissive, et il en va de même pour les genres. Il est interdit
+de faire rimer des mots dérivés l'un de l'autre (par exemple "justice" et
+"injustice") mais plint, faute de données, ne peut vérifier pas cette
+contrainte. Plint ne vérifie pas que les mots sont correctement orthographiés ou
+s'ils existent effectivement en français, ou si le poème a un sens de façon
+générale : par exemple, <q>Tatata tatati tatati tatata</q> est reconnu
+comme un alexandrin classique parfaitement valide.</p>
<h2 id="template">Comment faire pour définir ses propres modèles ?</h2>
@@ -155,6 +178,13 @@ options suivantes sont reconnues :</p>
<dd>Si cette valeur est false, le poème devrait avoir exactement la longueur
du modèle (ou un multiple, si repeat_ok est true), ne pas aller jusqu'à la fin
du modèle provoquera une erreur. Par défaut, la valeur est true.</dd>
+ <dt>phon_supposed_ok</dt>
+ <dd>Autoriser la liaison supposée pour les rimes (vrai par défaut).</dd>
+ <dt>eye_supposed_ok</dt>
+ <dd>Autoriser la liaison supposée pour les rimes pour l'œil (vrai par défaut).</dd>
+ <dt>eye_tolerance_ok</dt>
+ <dd>Autoriser les tolérances pour les rimes pour l'œil telles que "-é"/"-ai"
+ (vrai par défaut).</dd>
</dl>
<p>Désolé si tout cela semble un peu obscur. Vous pouvez regarder les modèles
@@ -180,8 +210,7 @@ prédéfinis pour comprendre comment ils fonctionnent :</p>
toutes lettres.</li>
<li>Si le compte de syllabes dans le vers est correct mais qu'une erreur de
métrique est signalée, vérifiez les hémistiches. Les césures incorrectes (qui
- coupent un mot ou se terminent par un son féminin) sont signalées avec des
- symboles comme '!', '?' ou ':'.</li>
+ coupent un mot ou se terminent par un son féminin) sont signalées.</li>
</ul>
<h2 id="pron">Comment la prononciation est-elle indiquée ?</h2>
@@ -234,47 +263,70 @@ source code.</p>
is the last word). The syllable count is <em>not</em> what you get with a
colloquial everyday reading. Some vowels clusters can count for two
syllables either naturally ("Léon") or through <em>diérèse</em> ("passion");
- plint uses an approximation of allowed <em>diérèses</em> trained on a corpus
- of classical texts. Some patterns such as "ées" or "ies" are forbidden
- altogether.</dd>
+ plint uses a tolerant approximation of allowed <em>diérèses</em> trained on a
+ corpus of classical texts.</dd>
<dt>Hemistiche</dt>
<dd>For classical alexandrines, the 12 syllables are separated in two groups
of 6 with an intermediate cesura (the <em>hémistiche</em>). The cesura must
not split a word and must not end in a weak sound (essentially, a non-elided
feminine ending).</dd>
<dt>Rhyme.</dt>
- <dd>The most well-known constraint is that verses must rhyme. The rhyming
- phonemes must include a vowel (eg. "tâte" and "bête" do not rhyme because
- their common phoneme suffix is [t] which does not include a vowel sound).
- plint enforces rhymes trough a liberal approximation of the pronunciation of
- verse endings. Plint merges [ɛ] et [ɛː] ("mettre" and "maître") and [a] and
- [ɑ] ("patte" and "pâte") but distinguishes [ɛ̃] and [œ̃] ("brin" and "brun") and
- [ɔ] and [o] ("cotte" and "côte"). When all classical rules are enforced, some
- rhymes such as <em>rime normande</em> and rhymes for a <em>liaison
- supposée</em> are accepted though they do not rhyme phonetically, and rhymes
- that do not match visually (such as "-é" and "-er") are refused, the details
- are complex. A word cannot rhyme with itself, but rhymes between homographs of
- a different grammatical nature are allowed (the number of occurrences of a
- word in a rhyme is bounded by the number of possible grammatical natures for
- this word).</dd>
+ <dd>The fact that verses must rhyme is the most well-known poetic constraint.
+ The rhyming phonemes must include a vowel (eg. "tâte" and "bête" do not rhyme
+ because their common phoneme suffix is [t] which does not include a vowel
+ sound). plint enforces rhymes trough a liberal approximation of the
+ pronunciation of verse endings. Plint merges [ɛ] et [ɛː] ("mettre" and
+ "maître") and [a] and [ɑ] ("patte" and "pâte") but distinguishes [ɛ̃] and [œ̃]
+ ("brin" and "brun") and [ɔ] and [o] ("cotte" and "côte"). When all classical
+ rules are enforced, some rhymes such as <em>rime normande</em> and rhymes for
+ a <em>liaison supposée</em> are accepted though they do not rhyme
+ phonetically.</dd>
+ <dt>Eye rhyme.</dt>
+ <dd>Classical rules require a minimal degree of <em>rime pour l'œil</em> (end
+ letter similarity); thus "-é" and "-er" do not rhyme, although common
+ tolerances exist, such as allowing "-é" and "-ai".</dd>
<dt>Rhyme genre.</dt>
<dd>In classical verse, rhymes must be made between feminine verse endings, or
masculine verse endings. A verse ending is feminine if it ends with a silent
"e", "es" or "ent", and masculine otherwise. plint enforces this, with a
liberal approximation to try to guess if an ending is indeed silent. Besides,
feminine and masculine rhymes must alternate.</dd>
+ <dt>Ambiguous patterns</dt>
+ <dd>Some patterns such as "-ées" or "-ies", or "-ée" and "-ie" followed by an
+ aspirated 'h' or a consonant, are forbidden by classical rules and
+ reported.</dd>
+ <dt>Hiatus</dt>
+ <dd>Juxtaposition of vowel sounds in two adjacent words with no mute 'e' in
+ the middle is forbidden and reported.</dd>
+ <dt>Repeated words</dt>
+ <dd>A word may not rhyme with itself, but may rhyme with a homograph of a
+ different grammatical nature, so plint will report as error any multiple
+ occurrences of a known word in a rhyme pattern if the number of occurrences is
+ more than the number of grammatical natures for the word.</dd>
+ <dt>Illegal characters</dt>
+ <dd>Characters that are not understood by plint, such as arabic numerals, are
+ reported as errors: on the one hand, they are probably best avoided in
+ classical poetry; on the second hand plint will probably misinterpret the
+ verse if it does not understand some of the characters.</dd>
+ <dt>Poem length</dt>
+ <dd>For some poetic forms such as the <em>sonnet</em>, the number of verses is
+ fixed, so an error is raised if the pattern was not completed to the end or
+ was repeated.</dd>
</dl>
<h2>What isn't checked by plint?</h2>
<p>Syllable count is performed by a liberal estimate which may allow some
invalid <em>diérèses</em> and <em>synérèses</em> to make the count match. It is
-(almost) not checked if <em>hémistiches</em> occur at a sensible grammatical
-position. Rhyme is computed by a liberal heuristic, as well as rhyme genre. More
-importantly, plint will not check if your poem makes any sense, does not contain
-spelling mistakes or has any poetic value; it only enforces formal constraints.
-For instance, <q>Tatata tatati tatati tatata</q> is recognized as a perfectly
-correct classical alexandrine.</p>
+not checked if <em>hémistiches</em> occur at a sensible grammatical position.
+Rhyme is computed by a liberal heuristic, as well as rhyme genre. Rhymes between
+words derived from one another, such as <em>justice</em> and <em>injustice</em>,
+but this is not checked by plint for lack of adequate data. More importantly,
+plint will not check if the words you use exist in French, or are correctly
+spelt, and if your poem makes any sense, does not contain spelling mistakes or
+ has any poetic value; it only enforces formal constraints. For instance,
+ <q>Tatata tatati tatati tatata</q> is recognized as a perfectly correct
+ classical alexandrine.</p>
<h2 id="template">How can I define my own templates?</h2>
@@ -331,6 +383,14 @@ form option:value and separated by spaces, with the following possibilities:</p>
<dd>If the value is false, the poem should match the length of the template
(or a multiple thereof is repeat_ok was set), and not completing a template
run will be an error. Default is true.</dd>
+ <dt>phon_supposed_ok</dt>
+ <dd>Allow <em>liaison supposée</em> for rhymes (default is true).</dd>
+ <dt>eye_supposed_ok</dt>
+ <dd>Allow <em>liaison supposée</em> for <em>rimes pour l'œil</em> (default is
+ true).</dd>
+ <dt>eye_tolerance_ok</dt>
+ <dd>Allow tolerances for <em>rimes pour l'œil</em> such as "-é"/"-ai" (default
+ is true).</dd>
</dl>
<p>Sorry if all of this is a bit obscure. You can have a look to the predefined
@@ -356,7 +416,7 @@ templates to understand how this works:</p>
full.</li>
<li>If the verse total is correct but a metric error is reported, check the
<em>hémistiche</em>. Wrong <em>hémistiches</em> (which cut a word or end in a
- feminine sound) are reported with symbols such as '!', '?' or ':'.</li>
+ feminine sound) are reported.</li>
</ul>
<h2 id="pron">How is pronunciation written?</h2>
diff --git a/views/index.html b/views/index.html
@@ -4,18 +4,17 @@
<form method="POST" action="check" id="bigform">
{% if lang == 'fr' %}
<p id="desc">plint est un outil pour vérifier la métrique et les rimes de poèmes en
- langue française. Entrez votre poème ici :</p>
+ langue française.</p>
{% else %}
- <p id="desc">plint is a tool to check the metric and rhymes of French verse.
- Input your poem here:</p>
+ <p id="desc">plint is a tool to check the metric and rhymes of French verse.</p>
{% endif %}
<label for="poem">
</label><br />
<textarea id="poem" name="poem"
{% if lang == 'fr' %}
- placeholder="Votre poème ici."
+ placeholder="Saisissez votre poème ici."
{% else %}
- placeholder="Your poem here."
+ placeholder="Input your poem here."
{% endif %}
></textarea>
<br/>
@@ -27,7 +26,7 @@
{% endif %}
</label><br />
<ul id="template">
- <li><input type="radio" name="template" value="classical"
+ <li><input onchange="showCustom(0)" type="radio" name="template" value="classical"
id="classical" checked="checked" />
<label for="classical">
{% if lang == 'fr' %}
@@ -38,7 +37,7 @@
rhyme genres.
{% endif %}
</label></li>
- <li><input type="radio" name="template" value="italian_abba" id="italian_abba" />
+ <li><input onchange="showCustom(0)" type="radio" name="template" value="italian_abba" id="italian_abba" />
<label for="italian_abba">
{% if lang == 'fr' %}
Sonnet italien classique d'alexandrins à rimes embrassées :<br/>deux quatrains
@@ -49,7 +48,7 @@
<em>tercets</em> with rhyme pattern CCD EED.
{% endif %}
</label></li>
- <li><input type="radio" name="template" value="italian_abab" id="italian_abab" />
+ <li><input onchange="showCustom(0)" type="radio" name="template" value="italian_abab" id="italian_abab" />
<label for="italian_abab">
{% if lang == 'fr' %}
Sonnet italien classique d'alexandrins à rimes croisées.
@@ -57,7 +56,7 @@
Classical Italian sonnet of alexandrine verse with <em>rimes croisées</em>.
{% endif %}
</label></li>
- <li><input type="radio" name="template" value="french_abba" id="french_abba" />
+ <li><input onchange="showCustom(0)" type="radio" name="template" value="french_abba" id="french_abba" />
<label for="french_abba">
{% if lang == 'fr' %}
Sonnet français classique d'alexandrins à rimes embrassées :<br/>deux
@@ -69,7 +68,7 @@
<em>tercets</em> with rhyme pattern CCD EDE.
{% endif %}
</label></li>
- <li><input type="radio" name="template" value="french_abab" id="french_abab" />
+ <li><input onchange="showCustom(0)" type="radio" name="template" value="french_abab" id="french_abab" />
<label for="french_abab">
{% if lang == 'fr' %}
Sonnet français classique d'alexandrins à rimes croisées.
@@ -77,7 +76,7 @@
Classical French sonnet of alexandrine verse with <em>rimes croisées</em>.
{% endif %}
</label></li>
- <li><input type="radio" name="template" value="alexandrin" id="alexandrin" />
+ <li><input onchange="showCustom(0)" type="radio" name="template" value="alexandrin" id="alexandrin" />
<label for="alexandrin">
{% if lang == 'fr' %}
Alexandrins sans hémistiche, sans vérification de la rime.
@@ -85,18 +84,18 @@
Alexandrine without <em>hémistiche</em> and without rhyme.
{% endif %}
</label></li>
- <li><input type="radio" name="template" value="custom" id="custom" />
+ <li><input onchange="showCustom(1)" type="radio" name="template" value="custom" id="custom" />
<label for="custom">
{% if lang == 'fr' %}
- Personnalisé (<a href="about#template">plus d'infos</a>)
+ Personnalisé (<a href="about#template" target="_blank">plus d'infos</a>)
{%else%}
- Custom (<a href="about#template">more info</a>)
+ Custom (<a href="about#template" target="_blank">more info</a>)
{%endif%}
</label>
<textarea name="custom_template" id="custom_template"
{% if lang == 'fr' %}
- placeholder="Entrez votre modèle personnalisé ici."
+ placeholder="Saisissez votre modèle personnalisé ici."
{% else %}
placeholder="Input a custom template here."
{% endif %}
@@ -104,7 +103,6 @@
</li>
</ul>
- <input id="reset" type="reset" />
<input id="submit" type="submit"
{% if lang == 'fr' %}
value="Valider"
diff --git a/views/page.html b/views/page.html
@@ -9,6 +9,8 @@
<meta name="description" content="plint French poetry checker" />
{% endif %}
<link rel="stylesheet" href="/static/main.css" type="text/css" media="screen" />
+ <script type="text/javascript" src="/static/script.js">
+ </script>
</head>
<body>
<header>