Sun, 23 Jun 2019 00:26:10 +0300
made problems be copied more nicely from the web front end
47 | 1 | import re |
2 | import linetypes | |
48 | 3 | import datetime |
47 | 4 | |
5 | class Header: | |
6 | def __init__(self): | |
7 | self.description = None | |
8 | self.name = None | |
9 | self.author = None | |
10 | self.username = None | |
11 | self.filetype = None | |
12 | self.qualifiers = None | |
13 | self.license = None | |
48 | 14 | self.help = '' |
47 | 15 | self.bfc = None |
16 | self.category = None | |
48 | 17 | self.keywords = '' |
47 | 18 | self.cmdline = None |
19 | self.history = [] | |
48 | 20 | self.first_occurrence = dict() |
21 | @property | |
22 | def valid(self): | |
23 | return True | |
69
a24c4490d9f2
added a check for keywords in non-parts
Teemu Piippo <teemu@hecknology.net>
parents:
67
diff
changeset
|
24 | @property |
a24c4490d9f2
added a check for keywords in non-parts
Teemu Piippo <teemu@hecknology.net>
parents:
67
diff
changeset
|
25 | def effective_filetype(self): |
a24c4490d9f2
added a check for keywords in non-parts
Teemu Piippo <teemu@hecknology.net>
parents:
67
diff
changeset
|
26 | if self.filetype.startswith('Unofficial_'): |
a24c4490d9f2
added a check for keywords in non-parts
Teemu Piippo <teemu@hecknology.net>
parents:
67
diff
changeset
|
27 | return self.filetype.rsplit('Unofficial_')[1] |
a24c4490d9f2
added a check for keywords in non-parts
Teemu Piippo <teemu@hecknology.net>
parents:
67
diff
changeset
|
28 | else: |
a24c4490d9f2
added a check for keywords in non-parts
Teemu Piippo <teemu@hecknology.net>
parents:
67
diff
changeset
|
29 | return self.filetype |
47 | 30 | |
31 | class BadHeader: | |
32 | def __init__(self, index, reason): | |
33 | self.index = index | |
34 | self.reason = reason | |
35 | def __repr__(self): | |
36 | return str.format( | |
37 | 'header.BadHeader(index = {index!r}, reason = {reason!r})', | |
38 | index = self.index, | |
39 | reason = self.reason, | |
40 | ) | |
48 | 41 | @property |
42 | def valid(self): | |
43 | return False | |
47 | 44 | |
45 | geometrical_types = [ | |
46 | linetypes.LineSegment, | |
47 | linetypes.Triangle, | |
48 | linetypes.Quadrilateral, | |
49 | linetypes.ConditionalLine, | |
50 | ] | |
51 | ||
56
ed6d39c59e56
fixed BFC INVERTNEXT being interpreted as a header command
Teemu Piippo <teemu@hecknology.net>
parents:
54
diff
changeset
|
52 | def is_invertnext(entry): |
ed6d39c59e56
fixed BFC INVERTNEXT being interpreted as a header command
Teemu Piippo <teemu@hecknology.net>
parents:
54
diff
changeset
|
53 | return isinstance(entry, linetypes.MetaCommand) \ |
ed6d39c59e56
fixed BFC INVERTNEXT being interpreted as a header command
Teemu Piippo <teemu@hecknology.net>
parents:
54
diff
changeset
|
54 | and entry.text == "BFC INVERTNEXT" |
ed6d39c59e56
fixed BFC INVERTNEXT being interpreted as a header command
Teemu Piippo <teemu@hecknology.net>
parents:
54
diff
changeset
|
55 | |
47 | 56 | def is_suitable_header_object(entry): |
56
ed6d39c59e56
fixed BFC INVERTNEXT being interpreted as a header command
Teemu Piippo <teemu@hecknology.net>
parents:
54
diff
changeset
|
57 | if is_invertnext(entry): |
ed6d39c59e56
fixed BFC INVERTNEXT being interpreted as a header command
Teemu Piippo <teemu@hecknology.net>
parents:
54
diff
changeset
|
58 | # BFC INVERTNEXT is not a header command anymore. |
ed6d39c59e56
fixed BFC INVERTNEXT being interpreted as a header command
Teemu Piippo <teemu@hecknology.net>
parents:
54
diff
changeset
|
59 | return False |
47 | 60 | return not any( |
61 | isinstance(entry, linetype) | |
62 | for linetype in [ | |
63 | *geometrical_types, | |
64 | linetypes.Comment, | |
65 | linetypes.Error, | |
66 | ] | |
67 | ) | |
68 | ||
69 | class HeaderError(Exception): | |
70 | def __init__(self, index, reason): | |
71 | self.index, self.reason = index, reason | |
72 | def __repr__(self): | |
73 | return str.format( | |
74 | 'HeaderError({index!r}, {reason!r})', | |
75 | index = self.index, | |
76 | reason = self.reason, | |
77 | ) | |
78 | def __str__(self): | |
79 | return reason | |
80 | ||
48 | 81 | class HistoryEntry: |
82 | def __init__(self, date, user, text): | |
83 | self.date, self.user, self.text = date, user, text | |
84 | def __repr__(self): | |
85 | return str.format( | |
86 | 'HistoryEntry({date!r}, {user!r}, {text!r})', | |
87 | date = self.date, | |
88 | user = self.user, | |
89 | text = self.text) | |
90 | ||
47 | 91 | class HeaderParser: |
92 | def __init__(self): | |
93 | self.model_body = None | |
94 | self.cursor = 0 | |
95 | self.problems = [] | |
96 | def parse(self, model_body): | |
97 | result = Header() | |
48 | 98 | self.result = result |
47 | 99 | self.order = [] |
100 | self.cursor = -1 | |
101 | self.model_body = model_body | |
102 | self.skip_to_next() | |
103 | result.description = self.current() | |
104 | self.skip_to_next() | |
52
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
105 | result.name = self.parse_pattern(r'^Name: (.+)$', 'name')[0] |
47 | 106 | self.skip_to_next() |
60 | 107 | result.author, result.username = self.parse_pattern(r'^Author: ([^ \[]*[^\[]+) (?:\[([^\]]+)\])?', 'author') |
47 | 108 | for header_entry in self.get_more_header_stuff(): |
109 | if self.try_to_match( | |
52
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
110 | r'^!LDRAW_ORG ' \ |
49 | 111 | r'((?:Unofficial_)?(?:' \ |
52
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
112 | r'Part|' \ |
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
113 | r'Subpart|' \ |
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
114 | r'Primitive|' \ |
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
115 | r'8_Primitive|' \ |
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
116 | r'48_Primitive|' \ |
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
117 | r'Shortcut' \ |
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
118 | r'))\s?' \ |
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
119 | r'(.*)$', |
47 | 120 | 'part type'): |
48 | 121 | result.filetype = self.groups[0] |
122 | result.qualifiers = re.findall(r'(?:Physical_Colour|Alias|ORIGINAL|UPDATE \d\d\d\d-\d\d)', self.groups[1]) | |
47 | 123 | elif self.try_to_match( |
52
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
124 | r'^!LICENSE (.+)$', |
47 | 125 | 'license'): |
48 | 126 | result.license = self.groups[0] |
127 | elif self.try_to_match( | |
52
cd2b4f3c1189
fix author parsing getting extra spaces in the name
Teemu Piippo <teemu@hecknology.net>
parents:
49
diff
changeset
|
128 | r'BFC (CERTIFY CW|CERTIFY CCW|NOCERTIFY)', |
48 | 129 | 'bfc'): |
130 | result.bfc = self.groups[0] | |
131 | elif self.try_to_match( | |
132 | r'!HISTORY (\d{4}-\d{2}-\d{2}) ([\[{][^\]}]+[\]}]) (.+)$', | |
133 | 'history'): | |
59
0f3e70a2bb4b
report invalid ISO dates instead of crashing
Teemu Piippo <teemu@hecknology.net>
parents:
56
diff
changeset
|
134 | try: |
0f3e70a2bb4b
report invalid ISO dates instead of crashing
Teemu Piippo <teemu@hecknology.net>
parents:
56
diff
changeset
|
135 | time_object = datetime.datetime.strptime( |
0f3e70a2bb4b
report invalid ISO dates instead of crashing
Teemu Piippo <teemu@hecknology.net>
parents:
56
diff
changeset
|
136 | self.groups[0], |
0f3e70a2bb4b
report invalid ISO dates instead of crashing
Teemu Piippo <teemu@hecknology.net>
parents:
56
diff
changeset
|
137 | '%Y-%m-%d', |
0f3e70a2bb4b
report invalid ISO dates instead of crashing
Teemu Piippo <teemu@hecknology.net>
parents:
56
diff
changeset
|
138 | ) |
0f3e70a2bb4b
report invalid ISO dates instead of crashing
Teemu Piippo <teemu@hecknology.net>
parents:
56
diff
changeset
|
139 | except ValueError: |
0f3e70a2bb4b
report invalid ISO dates instead of crashing
Teemu Piippo <teemu@hecknology.net>
parents:
56
diff
changeset
|
140 | self.parse_error("invalid ISO date in history") |
48 | 141 | result.history.append(HistoryEntry( |
59
0f3e70a2bb4b
report invalid ISO dates instead of crashing
Teemu Piippo <teemu@hecknology.net>
parents:
56
diff
changeset
|
142 | date = time_object.date(), |
48 | 143 | user = self.groups[1], |
144 | text = self.groups[2], | |
145 | )) | |
146 | elif self.try_to_match( | |
147 | r'!HELP (.+)', | |
148 | 'help'): | |
149 | if result.help: | |
150 | result.help += '\n' | |
151 | result.help += self.groups[0] | |
152 | elif self.try_to_match( | |
153 | r'!CATEGORY (.+)', | |
154 | 'category'): | |
155 | result.category = self.groups[0] | |
156 | elif self.try_to_match( | |
157 | r'!KEYWORDS (.+)', | |
158 | 'keywords'): | |
159 | if result.keywords: | |
160 | result.keywords += '\n' | |
161 | result.keywords += self.groups[0] | |
162 | elif self.try_to_match( | |
163 | r'!CMDLINE (.+)', | |
164 | 'cmdline'): | |
165 | result.cmdline = self.groups[0] | |
47 | 166 | else: |
48 | 167 | self.parse_error("couldn't understand header syntax: " + repr(header_entry.text)) |
67
afaa4d3bc3e5
complain if LDRAW_ORG line is missing
Teemu Piippo <teemu@hecknology.net>
parents:
60
diff
changeset
|
168 | if not result.filetype: |
afaa4d3bc3e5
complain if LDRAW_ORG line is missing
Teemu Piippo <teemu@hecknology.net>
parents:
60
diff
changeset
|
169 | self.parse_error('LDRAW_ORG line is missing') |
47 | 170 | return { |
171 | 'header': result, | |
172 | 'end-index': self.cursor + 1, | |
173 | } | |
174 | def parse_error(self, message): | |
175 | raise HeaderError(index = self.cursor, reason = message) | |
176 | def get_more_header_stuff(self): | |
177 | while True: | |
178 | self.cursor += 1 | |
179 | if self.cursor >= len(self.model_body): | |
180 | break | |
181 | entry = self.model_body[self.cursor] | |
182 | if not is_suitable_header_object(entry): | |
183 | break | |
184 | if isinstance(entry, linetypes.MetaCommand): | |
185 | yield entry | |
186 | def skip_to_next(self, *, spaces_expected = 0): | |
187 | while True: | |
188 | if self.cursor + 1 >= len(self.model_body): | |
54
0c686d10eb49
added tests for moved-to files and scaling in flat dimensions
Teemu Piippo <teemu@hecknology.net>
parents:
52
diff
changeset
|
189 | self.parse_error('file does not have a proper header') |
47 | 190 | self.cursor += 1 |
191 | entry = self.model_body[self.cursor] | |
192 | if not is_suitable_header_object(entry): | |
193 | self.parse_error('header is incomplete') | |
194 | if isinstance(entry, linetypes.MetaCommand): | |
195 | return | |
196 | def try_to_match(self, pattern, patterntype): | |
197 | try: | |
198 | self.groups = self.parse_pattern(pattern, patterntype) | |
48 | 199 | return True |
47 | 200 | except: |
201 | return False | |
202 | def current(self): | |
203 | entry = self.model_body[self.cursor] | |
204 | assert isinstance(entry, linetypes.MetaCommand) | |
205 | return entry.text | |
206 | def parse_pattern(self, pattern, description): | |
207 | match = re.search(pattern, self.current()) | |
208 | if match: | |
209 | self.order.append(description) | |
48 | 210 | if description not in self.result.first_occurrence: |
211 | self.result.first_occurrence[description] = self.cursor | |
47 | 212 | return match.groups() |
213 | else: | |
214 | self.parse_error(str.format("couldn't parse {}", description)) |