root/freeform/trunk/freeform/formtable.py

Revision 156, 8.5 kB (checked in by robin, 2 years ago)

tweaked the param list regex, added yield_commands

Line 
1 """formtable: compiles form descriptions  for use by `freeform.match`
2
3 """
4
5 import operator, re
6 from match import *
7 from match import _fieldtype_reprtab
8
9 __all__=[
10     'DefinitionErrors',
11     'compile', 'compile_source', 'yield_forms', 'yield_fields',
12     'create_formtable',
13     ]
14
15 class DefinitionErrors(Exception): pass
16
17 def create_formtable(commands, forms):
18     """Create a formtable.
19
20     create_formtable will take input of the format returned by `compile` and
21     produce a formtable. This can then be used to match the described phrases
22     in a reasonably efficient manner and, importantly, compensate reliably for
23     user finger trouble.
24    
25     Note: it should be possible to dynamicaly patch list and menu entries by
26     pokeing the formtable directly, (ie dynamicaly filled menus). However
27     support for easily doing this is incomplete.
28     """
29    
30     maxfieldtypes = len([k for k in _fieldtype_reprtab.keys() if k >=0])
31     longestformlen=0
32     form2command=[form[0] for form in forms]
33     form2fieldtypes=[]
34     form2fieldnames=[]
35     form2fieldselectvalues=[]
36     formtable=[
37         form2fieldtypes, form2fieldnames,
38         form2fieldselectvalues]
39     for formid, (cmd, form) in enumerate(forms):
40         fieldtypes = map(operator.itemgetter(0), form)
41         fieldnames = map(operator.itemgetter(1), form)
42         fieldselectvalues = map(operator.itemgetter(2), form)
43         formproperties=[
44             fieldtypes, fieldnames, fieldselectvalues]
45         for iproperty, property in enumerate(formproperties):               
46             formtable[iproperty].append(property)
47
48         if longestformlen < len(form):
49             longestformlen = len(form)
50            
51     for propertytable in formtable:
52         for formproperties in propertytable:
53             formproperties.extend([None] * (longestformlen - len(formproperties)))
54
55     formcount = len(forms)
56    
57     # compute the transpose
58     tformtable = [[[] for ifield in range(0,longestformlen)]
59                     for propertytable in formtable]
60     for iproperty in range(0, len(formtable)):
61         for ifield in range(0, longestformlen):
62             for formid in range(0, formcount):
63                 tformtable[iproperty][ifield].append(
64                     formtable[iproperty][formid][ifield])
65     field2fieldtypes = tformtable[0]
66     field2fieldnames = tformtable[1]
67     field2selectvalues = tformtable[2]
68     return dict([
69         ('formcount', formcount),
70         ('maxfieldcount', longestformlen),
71         ('maxfieldtypes',maxfieldtypes),
72         ('form2command',form2command),
73         ('form2fieldtypes',form2fieldtypes),
74         ('form2fieldnames', form2fieldnames),
75         ('form2fieldselectvalues', form2fieldselectvalues),
76         ('field2fieldtypes', field2fieldtypes),
77         ('field2fieldnames', field2fieldnames),
78         ('field2selectvalues',field2selectvalues)])
79
80 MATCH_CMD_START=re.compile(r'(?P<NAME>[a-zA-Z_]\w*)\s*:\s*').match
81 MATCH_CMD_END=re.compile(r';\s*').match
82 MATCH_CEND_OR_FSEP=re.compile(r'(?P<CEND>;\s*)|(?P<FSEP>,\s*)').match
83 _CENDCHAR=';'
84
85 # there is nothing critical about the leagal start characters for keywords.
86 # freeform doesn't care. the only hard restriction, due to the form compiler,
87 # rather than the match algorithim, is that a keyword may not
88 # contain any of '{', ';'. Note that it _can_ contain any punctuation
89 # character, including '.','@', etc.. If you _realy_ need every last drop
90 # of flexibility you could  hand code your form definition.
91
92 MATCH_KW=re.compile(r'(?P<NAME>[-\.]?\w+[^\s;\{]*\w)\s*').match
93
94 MATCH_WW=re.compile(r'\{\s*(?P<NAME>\w*?)\s*\(\s*s\s*\)\s*\}\s*').match
95 MATCH_WO=re.compile(r'\{\s*(?P<NAME>\w*?)\s*\}\s*').match
96 MATCH_ME=re.compile(
97         r'\{\s*(?P<NAME>\w*?)\s*\(\s*menu\s+'
98         r'(?P<variants>\w*?)\s*\)\s*\}\s*').match
99 MATCH_LI_START=re.compile(r'\{\s*(?P<NAME>\w*?)\s*\(\s*list\s+').match
100 MATCH_LM_menu=re.compile(r'\s*menu\s*(?P<variants>\w+)\s*').match
101 MATCH_paramlist=re.compile(r'((\s*\w+\s*))(,(\s*[^\s,]+\s*))*[.]').match
102 MATCH_LILM_END=re.compile(r'\s*\)\s*\}\s*').match
103
104 def compile(sources, commands=None, forms=None):
105     if commands is None: commands = {}
106     if forms is None: forms = []
107     brokenforms=[]
108    
109     for i, source in enumerate(sources):
110         source = source.strip()
111         (pos, c, f, e) = compile_source(
112             source, commands, forms)
113         e and brokenforms.extend(e)
114     if brokenforms:
115         raise DefinitionErrors(commands, forms, brokenforms)
116     return (commands,forms),brokenforms
117
118 def compile_source(source, commands=None, forms=None, pos=0):
119     if commands is None: commands={}
120     if forms is None: forms = []
121     brokenforms=[]
122     # easier to strip the coment lines in one pass
123     source = '\n'.join((line for line in source.split('\n')
124         if not line.startswith('#')))
125     while 1:
126         match=MATCH_CMD_START(source[pos:])
127         if not match:
128             return pos, commands, forms, brokenforms
129         pos += match.span()[-1]
130         command=match.group('NAME')
131         commandforms=commands.setdefault(command, [])
132         for pos, form, error in yield_forms(source, pos):
133             # error may be informative warning. if the error was
134             # unrecoverable the last field considered will be marked
135             # invalid
136             if form and form[-1][0] is not FIELDTYPE_INVALID:
137                 commandforms.append(len(forms))
138                 forms.append((command, form))
139             else:
140                 brokenforms.append((command, form))
141
142 def yield_forms(source, pos=0):
143     CENDCHAR=_CENDCHAR
144     while 1:
145         form=[]
146         for pos, fieldtype, name, details in yield_fields(source, pos):
147             form.append((fieldtype, name, details))
148             if fieldtype is FIELDTYPE_INVALID:       
149                 yield pos, form, details[0]
150                 return
151         match = MATCH_CEND_OR_FSEP(source[pos:])
152         if not match:
153             yield pos, form, 'ERR_GRAMATICAL1:unterminated form definition list'
154             return
155         group = match.group('FSEP') or match.group('CEND')
156         pos += match.span()[-1]
157         yield pos, form, None
158         if group[0] == CENDCHAR:
159             return
160        
161 def yield_fields(source, pos=0):
162     produced = None
163     while 1:
164         for (matcher,fieldtype) in [
165             (MATCH_WW, FIELDTYPE_WORDS),
166             (MATCH_WO, FIELDTYPE_WORD),
167             (MATCH_ME, FIELDTYPE_MENU),
168             (MATCH_LI_START, FIELDTYPE_UNKNOWN),
169             (MATCH_KW, FIELDTYPE_KEYWORD)]:
170             match=matcher(source[pos:])
171             if not match:
172                 continue
173             pos += match.span()[-1]
174             name=match.group('NAME')
175             if matcher is not MATCH_LI_START:
176                 produced = (fieldtype, name,
177                     (fieldtype is FIELDTYPE_MENU
178                         and [c for c in match.group('variants')])
179                     or
180                     (fieldtype is FIELDTYPE_KEYWORD and [name])
181                     or [])
182                 break
183             listmatch=MATCH_paramlist(source[pos:])
184             if not listmatch:
185                 produced = (FIELDTYPE_INVALID, name,
186                     ['TOKENERROR1:bad list or listmenu field', source[pos:],
187                         [match]])
188                 break
189             listparams = [variant.strip() for variant in
190                     listmatch.group(0).split('.')[0].split(',')]
191            
192             pos += listmatch.span()[-1]
193             menumatch=MATCH_LM_menu(source[pos:])
194             if not menumatch:
195                 menuend=MATCH_LILM_END(source[pos:])
196                 if not menuend:
197                     produced = (FIELDTYPE_INVALID, name,
198                             ['TOKENERROR2:badly terminated list parameters',
199                                 source[:pos],
200                             [match, listmatch, listparams]])
201                     break
202                 pos += menuend.span()[-1]
203                 produced = (FIELDTYPE_LIST, name, listparams)
204                 break
205             pos += menumatch.span()[-1]
206             listmenuend=MATCH_LILM_END(source[pos:])
207             if not listmenuend:
208                 produced = (FIELDTYPE_INVALID, name,
209                     ['TOKENERROR4:badly terminated menu parameters in listmenu field',
210                     source[pos:],
211                         [match,listmatch,menumatch]])
212                 break
213             pos+=listmenuend.span()[-1]
214             produced =  (FIELDTYPE_LISTMENU, name,
215                 [listparams, [c for c in menumatch.group('variants')]])
216             break
217                
218         if produced:
219             yield (pos,)+produced
220             produced = None
221         else:
222             return
Note: See TracBrowser for help on using the browser.