plint

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

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:
.gitignore | 1-
TODO | 19++++++++++---------
common.py | 12++++++++----
error.py | 36++++++++++++++++++------------------
plint_irc.py | 4++++
res/messages_fr.po | 62+++++++++++++++++++++++++++++++++-----------------------------
rhyme.py | 107++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
static/main.css | 1+
static/script.js | 7+++++++
template.py | 118++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
test/letters | 4++++
test/letters.tpl | 4++++
verse.py | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
versetest.py | 9++++++++-
views/about.html | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
views/index.html | 30++++++++++++++----------------
views/page.html | 2++
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&nbsp;?</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&nbsp;: 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")&nbsp;; 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")&nbsp;; 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&nbsp;?</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&nbsp;: 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&nbsp;: 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&nbsp;?</h2> @@ -155,6 +178,13 @@ options suivantes sont reconnues&nbsp;:</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&nbsp;:</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&nbsp;?</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&nbsp;:</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&nbsp;:<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&nbsp;:<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>