OSDN Git Service

Icelandic (is):
[nvdajp/nvdajp.git] / keyCommandsDoc.py
1 #keyCommandsDoc.py\r
2 #A part of NonVisual Desktop Access (NVDA)\r
3 #This file is covered by the GNU General Public License.\r
4 #See the file COPYING for more details.\r
5 #Copyright (C) 2010 James Teh <jamie@jantrid.net>\r
6 \r
7 """Utilities related to NVDA Key Commands documents.\r
8 """\r
9 \r
10 import os\r
11 import codecs\r
12 import re\r
13 import txt2tags\r
14 \r
15 LINE_END = u"\r\n"\r
16 \r
17 class KeyCommandsError(Exception):\r
18         """Raised due to an error encountered in the User Guide related to generation of the Key Commands document.\r
19         """\r
20 \r
21 class KeyCommandsMaker(object):\r
22         """Generates the Key Commands document from the User Guide.\r
23         To generate a Key Commands document, create an instance and then call L{make} on it.\r
24         \r
25         Generation of the Key Commands document requires certain commands to be included in the user guide.\r
26         These commands must begin at the start of the line and take the form::\r
27                 %kc:command: arg\r
28         \r
29         The kc:title command must appear first and specifies the title of the Key Commands document. For example::\r
30                 %kc:title: NVDA Key Commands\r
31         \r
32         The kc:includeconf command allows you to insert a txt2tags includeconf command in the Key Commands document. For example::\r
33                 %kc:includeconf: ../ar.t2tconf\r
34         You may use multiple kc:includeconf commands, but they must appear before any of the other commands below.\r
35         \r
36         The rest of these commands are used to include key commands into the document.\r
37         Appropriate headings from the User Guide will be included implicitly.\r
38         \r
39         The kc:beginInclude command begins a block of text which should be included verbatim.\r
40         The block ends at the kc:endInclude command.\r
41         For example::\r
42                 %kc:beginInclude\r
43                 || Name | Desktop command | Laptop command | Description |\r
44                 ...\r
45                 %kc:endInclude\r
46         \r
47         The kc:settingsSection command indicates the beginning of a section documenting individual settings.\r
48         It specifies the header row for a table summarising the settings indicated by the kc:setting command (see below).\r
49         In order, it must consist of a name column, a column for each keyboard layout and a description column.\r
50         For example::\r
51                 %kc:settingsSection: || Name | Desktop command | Laptop command | Description |\r
52         \r
53         The kc:setting command indicates a section for an individual setting.\r
54         It must be followed by:\r
55                 * A heading containing the name of the setting;\r
56                 * A table row for each keyboard layout, or if the key is common to all layouts, a single line of text specifying the key after a colon;\r
57                 * A blank line; and\r
58                 * A line describing the setting.\r
59         For example::\r
60                 %kc:setting\r
61                 ==== Braille Tethered To ====\r
62                 | Desktop command | NVDA+control+t |\r
63                 | Laptop Command | NVDA+control+t |\r
64                 This option allows you to choose whether the braille display will follow the system focus, or whether it follows the navigator object / review cursor.\r
65         """\r
66 \r
67         t2tRe = None\r
68         RE_COMMAND = re.compile(r"^%kc:(?P<cmd>[^:\s]+)(?:: (?P<arg>.*))?$")\r
69         KCSECT_HEADER = 0\r
70         KCSECT_CONFIG = 1\r
71         KCSECT_BODY = 2\r
72 \r
73         @classmethod\r
74         def _initClass(cls):\r
75                 if cls.t2tRe:\r
76                         return\r
77                 # Only fetch this once.\r
78                 cls.t2tRe = txt2tags.getRegexes()\r
79 \r
80         def __init__(self, userGuideFilename,keyCommandsFileName):\r
81                 """Constructor.\r
82                 @param userGuideFilename: The file name of the User Guide to be used as input.\r
83                 @type userGuideFilename: str\r
84                 @param keyCommandsFilename: The file name of the key commands file to be output. \r
85                 @type keyCommandsFilename: str\r
86                 """\r
87                 self._initClass()\r
88                 self.ugFn = userGuideFilename\r
89                 #: The file name of the Key Commands document that will be generated.\r
90                 #: This will be in the same directory as the User Guide.\r
91                 self.kcFn = keyCommandsFileName\r
92                 #: The current section of the key commands file.\r
93                 self._kcSect = self.KCSECT_HEADER\r
94                 #: The current stack of headings.\r
95                 self._headings = []\r
96                 #: The 0 based level of the last heading in L{_headings} written to the key commands file.\r
97                 self._kcLastHeadingLevel = -1\r
98                 #: Whether lines which aren't commands should be written to the key commands file as is.\r
99                 self._kcInclude = False\r
100                 #: The header row for settings sections.\r
101                 self._settingsHeaderRow = None\r
102                 #: The number of layouts for settings in a settings section.\r
103                 self._settingsNumLayouts = 0\r
104                 #: The current line number being processed, used to present location of syntax errors\r
105                 self._lineNum = 0\r
106 \r
107         def make(self):\r
108                 """Generate the Key Commands document.\r
109                 @postcondition: If the User Guide contains appropriate commands, the Key Commands document will be generated and saved as L{kcFn}.\r
110                         Otherwise, no file will be generated.\r
111                 @return: C{True} if a document was generated, C{False} otherwise.\r
112                 @rtype: bool\r
113                 @raise IOError:\r
114                 @raise KeyCommandsError:\r
115                 """\r
116                 tKcFn=self.kcFn+'__'\r
117                 self._ug = codecs.open(self.ugFn, "r", "utf-8-sig")\r
118                 self._kc = codecs.open(tKcFn, "w", "utf-8-sig")\r
119 \r
120                 success=False\r
121                 with self._ug, self._kc:\r
122                         self._make()\r
123                         success=self._kc.tell() > 0\r
124                 if success:\r
125                         os.rename(tKcFn,self.kcFn)\r
126                 else:\r
127                         os.remove(tKcFn)\r
128                 return success\r
129 \r
130         def _make(self):\r
131                 for line in self._ug:\r
132                         self._lineNum += 1\r
133                         line = line.rstrip()\r
134                         m = self.RE_COMMAND.match(line)\r
135                         if m:\r
136                                 self._command(**m.groupdict())\r
137                                 continue\r
138 \r
139                         m = self.t2tRe["numtitle"].match(line)\r
140                         if m:\r
141                                 self._heading(m)\r
142                                 continue\r
143 \r
144                         if self._kcInclude:\r
145                                 self._kc.write(line + LINE_END)\r
146 \r
147         def _command(self, cmd=None, arg=None):\r
148                 # Handle header commands.\r
149                 if cmd == "title":\r
150                         if self._kcSect > self.KCSECT_HEADER:\r
151                                 raise KeyCommandsError("%d, title command is not valid here" % self._lineNum)\r
152                         # Write the title and two blank lines to complete the txt2tags header section.\r
153                         self._kc.write(arg + LINE_END * 3)\r
154                         self._kcSect = self.KCSECT_CONFIG\r
155                         self._kc.write("%%!includeconf: ../global.t2tconf%s" % LINE_END)\r
156                         return\r
157                 elif self._kcSect == self.KCSECT_HEADER:\r
158                         raise KeyCommandsError("%d, title must be the first command" % self._lineNum)\r
159                 elif cmd == "includeconf":\r
160                         if self._kcSect > self.KCSECT_CONFIG:\r
161                                 raise KeyCommandsError("%d, includeconf command is not valid here" % self._lineNum)\r
162                         self._kc.write("%%!includeconf: %s%s" % (arg, LINE_END))\r
163                         return\r
164                 elif self._kcSect == self.KCSECT_CONFIG:\r
165                         self._kc.write(LINE_END)\r
166                         self._kcSect = self.KCSECT_BODY\r
167 \r
168                 if cmd == "beginInclude":\r
169                         self._writeHeadings()\r
170                         self._kcInclude = True\r
171                 elif cmd == "endInclude":\r
172                         self._kcInclude = False\r
173                         self._kc.write(LINE_END)\r
174 \r
175                 elif cmd == "settingsSection":\r
176                         # The argument is the table header row for the settings section.\r
177                         self._settingsHeaderRow = arg\r
178                         # There are name and description columns.\r
179                         # Each of the remaining columns provides keystrokes for one layout.\r
180                         # There's one less delimiter than there are columns, hence subtracting 1 instead of 2.\r
181                         self._settingsNumLayouts = arg.strip("|").count("|") - 1\r
182                 elif cmd == "setting":\r
183                         self._handleSetting()\r
184 \r
185                 else:\r
186                         raise KeyCommandsError("%d, Invalid command %s" % (self._lineNum, cmd))\r
187 \r
188         def _areHeadingsPending(self):\r
189                 return self._kcLastHeadingLevel < len(self._headings) - 1\r
190 \r
191         def _writeHeadings(self):\r
192                 level = self._kcLastHeadingLevel + 1\r
193                 # Only write headings we haven't yet written.\r
194                 for level, heading in enumerate(self._headings[level:], level):\r
195                         # We don't want numbered headings in the output.\r
196                         label=heading.group("label")\r
197                         headingText = u"{id}{txt}{id}{label}".format(\r
198                                 id="=" * len(heading.group("id")),\r
199                                 txt=heading.group("txt"),\r
200                                 label="[%s]" % label if label else "")\r
201                         # Write the heading and a blank line.\r
202                         self._kc.write(headingText + LINE_END * 2)\r
203                 self._kcLastHeadingLevel = level\r
204 \r
205         def _heading(self, m):\r
206                 # We work with 0 based heading levels.\r
207                 level = len(m.group("id")) - 1\r
208                 try:\r
209                         del self._headings[level:]\r
210                 except IndexError:\r
211                         pass\r
212                 self._headings.append(m)\r
213                 self._kcLastHeadingLevel = min(self._kcLastHeadingLevel, level - 1)\r
214 \r
215         def _handleSetting(self):\r
216                 if not self._settingsHeaderRow:\r
217                         raise KeyCommandsError("%d, setting command cannot be used before settingsSection command" % self._lineNum)\r
218 \r
219                 if self._areHeadingsPending():\r
220                         # There are new headings to write.\r
221                         # If there was a previous settings table, it ends here, so write a blank line.\r
222                         self._kc.write(LINE_END)\r
223                         self._writeHeadings()\r
224                         # New headings were written, so we need to output the header row.\r
225                         self._kc.write(self._settingsHeaderRow + LINE_END)\r
226 \r
227                 # The next line should be a heading which is the name of the setting.\r
228                 line = next(self._ug)\r
229                 self._lineNum += 1\r
230                 m = self.t2tRe["title"].match(line)\r
231                 if not m:\r
232                         raise KeyCommandsError("%d, setting command must be followed by heading" % self._lineNum)\r
233                 name = m.group("txt")\r
234 \r
235                 # The next few lines should be table rows for each layout.\r
236                 # Alternatively, if the key is common to all layouts, there will be a single line of text specifying the key after a colon.\r
237                 keys = []\r
238                 for layout in xrange(self._settingsNumLayouts):\r
239                         line = next(self._ug).strip()\r
240                         self._lineNum += 1\r
241                         if ":" in line:\r
242                                 keys.append(line.split(":", 1)[1].strip())\r
243                                 break\r
244                         elif not self.t2tRe["table"].match(line):\r
245                                 raise KeyCommandsError("%d, setting command: There must be one table row for each keyboard layout" % self._lineNum)\r
246                         # This is a table row.\r
247                         # The key will be the second column.\r
248                         # TODO: Error checking.\r
249                         key = line.strip("|").split("|")[1].strip()\r
250                         keys.append(key)\r
251                 if 1 == len(keys) < self._settingsNumLayouts:\r
252                         # The key has only been specified once, so it is the same in all layouts.\r
253                         key = keys[0]\r
254                         keys[1:] = (key for layout in xrange(self._settingsNumLayouts - 1))\r
255 \r
256                 # There should now be a blank line.\r
257                 line = next(self._ug).strip()\r
258                 self._lineNum += 1\r
259                 if line:\r
260                         raise KeyCommandsError("%d, setting command: The keyboard shortcuts must be followed by a blank line. Multiple keys must be included in a table. Erroneous key: %s" % (self._lineNum, key))\r
261 \r
262                 # Finally, the next line should be the description.\r
263                 desc = next(self._ug).strip()\r
264                 self._lineNum += 1\r
265 \r
266                 self._kc.write(u"| {name} | {keys} | {desc} |{lineEnd}".format(\r
267                         name=name,\r
268                         keys=u" | ".join(keys),\r
269                         desc=desc, lineEnd=LINE_END))\r
270 \r
271         def remove(self):\r
272                 """Remove the generated Key Commands document.\r
273                 """\r
274                 try:\r
275                         os.remove(self.kcFn)\r
276                 except OSError:\r
277                         pass\r