|
19 | 19 | # along with this program; if not, write to the Free Software
|
20 | 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
21 | 21 |
|
22 |
| -from collections import ( |
23 |
| - OrderedDict, |
24 |
| - namedtuple, |
25 |
| -) |
26 |
| -from collections.abc import MutableSequence |
27 |
| -from enum import IntEnum |
28 |
| -import html |
29 |
| - |
30 |
| - |
31 |
| -try: |
32 |
| - from markdown import markdown |
33 |
| -except ImportError: |
34 |
| - markdown = None |
35 |
| - |
36 | 22 | from picard.const import PICARD_URLS
|
37 |
| -from picard.i18n import ( |
38 |
| - N_, |
39 |
| - gettext as _, |
40 |
| -) |
41 |
| -from picard.options import get_option_title |
42 |
| - |
43 |
| - |
44 |
| -DocumentLink = namedtuple('DocumentLink', ('title', 'link')) |
45 |
| - |
46 |
| - |
47 |
| -def _markdown(text: str): |
48 |
| - text = html.escape(text) |
49 |
| - if markdown is None: |
50 |
| - return '<p>' + text.replace('\n', '<br />') + '</p>' |
51 |
| - return markdown(text) |
52 |
| - |
53 |
| - |
54 |
| -class Section(IntEnum): |
55 |
| - notes = 1 |
56 |
| - options = 2 |
57 |
| - links = 3 |
58 |
| - see_also = 4 |
59 |
| - |
60 |
| - |
61 |
| -SectionInfo = namedtuple('Section', ('title', 'tagvar_func')) |
62 |
| -SECTIONS = { |
63 |
| - Section.notes: SectionInfo(N_('Notes'), 'notes'), |
64 |
| - Section.options: SectionInfo(N_('Option Settings'), 'related_options_titles'), |
65 |
| - Section.links: SectionInfo(N_('Links'), 'links'), |
66 |
| - Section.see_also: SectionInfo(N_('See Also'), 'see_alsos'), |
67 |
| -} |
68 |
| - |
69 |
| -TEXT_NO_DESCRIPTION = N_('No description available.') |
70 |
| - |
71 |
| -ATTRIB2NOTE = OrderedDict( |
72 |
| - is_multi_value=N_('multi-value variable'), |
73 |
| - is_preserved=N_('preserved read-only'), |
74 |
| - not_script_variable=N_('not for use in scripts'), |
75 |
| - is_calculated=N_('calculated'), |
76 |
| - is_file_info=N_('info from audio file'), |
77 |
| - not_from_mb=N_('not provided from MusicBrainz data'), |
78 |
| - not_populated_by_picard=N_('not populated by stock Picard'), |
| 23 | +from picard.i18n import N_ |
| 24 | +from picard.tags.tagvar import ( |
| 25 | + DocumentLink, |
| 26 | + TagVar, |
| 27 | + TagVars, |
79 | 28 | )
|
80 | 29 |
|
81 | 30 |
|
82 |
| -class TagVar: |
83 |
| - def __init__( |
84 |
| - self, name, shortdesc=None, longdesc=None, additionaldesc=None, |
85 |
| - is_preserved=False, is_hidden=False, is_script_variable=True, is_tag=True, is_calculated=False, |
86 |
| - is_file_info=False, is_from_mb=True, is_populated_by_picard=True, is_multi_value=False, |
87 |
| - see_also=None, related_options=None, doc_links=None |
88 |
| - ): |
89 |
| - """ |
90 |
| - shortdesc: Short description (typically one or two words) in title case that is suitable |
91 |
| - for a column header. |
92 |
| - longdesc: Brief description in sentence case describing the tag/variable. This should |
93 |
| - be similar (within reasonable length constraints) to the description in the Picard User |
94 |
| - Guide documentation, and will be used as a tooltip when reviewing a script. May |
95 |
| - contain markdown. |
96 |
| - additionaldesc: Additional description which might include more details or examples. May |
97 |
| - contain markdown. |
98 |
| - is_preserved: the tag is preserved (boolean, default: False) |
99 |
| - is_hidden: the tag is "hidden", name will be prefixed with "~" (boolean, default: False) |
100 |
| - is_script_variable: the tag can be used as script variable (boolean, default: True) |
101 |
| - is_tag: the tag is an actual tag (not a calculated or derived one) (boolean, default: True) |
102 |
| - is_calculated: the tag is obtained by external calculation (boolean, default: False) |
103 |
| - is_file_info: the tag is a file information, displayed in file info box (boolean, default: False) |
104 |
| - is_from_mb: the tag information is provided from the MusicBrainz database (boolean, default: True) |
105 |
| - is_populated_by_picard: the tag information is populated by stock Picard (boolean, default: True) |
106 |
| - is_multi_value: the tag is a multi-value variable (boolean, default: False) |
107 |
| - see_also: an iterable containing ids of related tags |
108 |
| - related_options: an iterable containing the related option settings (see picard/options.py) |
109 |
| - doc_links: an iterable containing links to external documentation (DocumentLink tuples) |
110 |
| - """ |
111 |
| - self._name = name |
112 |
| - self._shortdesc = shortdesc |
113 |
| - self._longdesc = longdesc |
114 |
| - self._additionaldesc = additionaldesc |
115 |
| - self.is_preserved = is_preserved |
116 |
| - self.is_hidden = is_hidden |
117 |
| - self.is_script_variable = is_script_variable |
118 |
| - self.is_tag = is_tag |
119 |
| - self.is_calculated = is_calculated |
120 |
| - self.is_file_info = is_file_info |
121 |
| - self.is_from_mb = is_from_mb |
122 |
| - self.is_populated_by_picard = is_populated_by_picard |
123 |
| - self.is_multi_value = is_multi_value |
124 |
| - self.see_also = see_also |
125 |
| - self.related_options = related_options |
126 |
| - self.doc_links = doc_links |
127 |
| - |
128 |
| - @property |
129 |
| - def shortdesc(self): |
130 |
| - """default to name""" |
131 |
| - if self._shortdesc: |
132 |
| - return self._shortdesc.strip() |
133 |
| - return str(self) |
134 |
| - |
135 |
| - @property |
136 |
| - def longdesc(self): |
137 |
| - """default to shortdesc""" |
138 |
| - if self._longdesc: |
139 |
| - return self._longdesc.strip() |
140 |
| - return self.shortdesc |
141 |
| - |
142 |
| - @property |
143 |
| - def additionaldesc(self): |
144 |
| - if not self._additionaldesc: |
145 |
| - return '' |
146 |
| - return self._additionaldesc.strip() |
147 |
| - |
148 |
| - @property |
149 |
| - def not_from_mb(self): |
150 |
| - return not self.is_from_mb |
151 |
| - |
152 |
| - @property |
153 |
| - def not_script_variable(self): |
154 |
| - return not self.is_script_variable |
155 |
| - |
156 |
| - @property |
157 |
| - def not_populated_by_picard(self): |
158 |
| - return not self.is_populated_by_picard |
159 |
| - |
160 |
| - def __str__(self): |
161 |
| - """hidden marked with a prefix""" |
162 |
| - if self.is_hidden: |
163 |
| - return '~' + self._name |
164 |
| - else: |
165 |
| - return self._name |
166 |
| - |
167 |
| - def script_name(self): |
168 |
| - """In scripts, ~ prefix is replaced with _ for hidden variables""" |
169 |
| - if self.is_hidden: |
170 |
| - return '_' + self._name |
171 |
| - else: |
172 |
| - return self._name |
173 |
| - |
174 |
| - |
175 |
| -class TagVars(MutableSequence): |
176 |
| - """Mutable sequence for TagVar items |
177 |
| - It maintains an internal dict object for display names. |
178 |
| - Also it doesn't allow to add a TagVar of the same name more than once. |
179 |
| - """ |
180 |
| - def __init__(self, *tagvars): |
181 |
| - self._items = [] |
182 |
| - self._name2item = dict() |
183 |
| - self.extend(tagvars) |
184 |
| - |
185 |
| - def __len__(self): |
186 |
| - return len(self._items) |
187 |
| - |
188 |
| - def __getitem__(self, index): |
189 |
| - return self._items[index] |
190 |
| - |
191 |
| - def _get_name(self, tagvar): |
192 |
| - if not isinstance(tagvar, TagVar): |
193 |
| - raise TypeError(f"Value isn't a TagVar instance: {tagvar}") |
194 |
| - name = str(tagvar) |
195 |
| - if name in self._name2item: |
196 |
| - raise ValueError(f"Already an item with same name: {name}") |
197 |
| - return name |
198 |
| - |
199 |
| - def __setitem__(self, index, tagvar): |
200 |
| - name = self._get_name(tagvar) |
201 |
| - self._name2item[name] = self._items[index] = tagvar |
202 |
| - |
203 |
| - def __delitem__(self, index): |
204 |
| - name = str(self._items[index]) |
205 |
| - del self._items[index] |
206 |
| - del self._name2item[name] |
207 |
| - |
208 |
| - def insert(self, index, tagvar): |
209 |
| - name = self._get_name(tagvar) |
210 |
| - self._items.insert(index, tagvar) |
211 |
| - self._name2item[name] = self._items[index] |
212 |
| - |
213 |
| - def __repr__(self): |
214 |
| - return f"TagVars({self._items!r})" |
215 |
| - |
216 |
| - def item_from_name(self, name): |
217 |
| - if ':' in name: |
218 |
| - name, tagdesc = name.split(':', 1) |
219 |
| - else: |
220 |
| - tagdesc = None |
221 |
| - |
222 |
| - if name and name.startswith('_'): |
223 |
| - search_name = name.replace('_', '~', 1) |
224 |
| - elif name and name.startswith('~'): |
225 |
| - search_name = name |
226 |
| - name = name.replace('~', '_') |
227 |
| - else: |
228 |
| - search_name = name |
229 |
| - |
230 |
| - item: TagVar = self._name2item.get(search_name, None) |
231 |
| - |
232 |
| - return name, tagdesc, search_name, item |
233 |
| - |
234 |
| - def script_name_from_name(self, name): |
235 |
| - tagname, tagdesc, search_name, item = self.item_from_name(name) |
236 |
| - if item: |
237 |
| - return str(item) |
238 |
| - return None |
239 |
| - |
240 |
| - def display_name(self, name): |
241 |
| - name, tagdesc, search_name, item = self.item_from_name(name) |
242 |
| - |
243 |
| - if item and item.shortdesc: |
244 |
| - title = _(item.shortdesc) |
245 |
| - else: |
246 |
| - title = search_name |
247 |
| - if tagdesc: |
248 |
| - return '%s [%s]' % (title, tagdesc) |
249 |
| - else: |
250 |
| - return title |
251 |
| - |
252 |
| - @staticmethod |
253 |
| - def _format_display(name, content, tagdesc): |
254 |
| - fmt_tagdesc = _("<p><em>%{name}%</em> [{tagdesc}]</p>{content}") |
255 |
| - fmt_normal = _("<p><em>%{name}%</em></p>{content}") |
256 |
| - fmt = fmt_tagdesc if tagdesc else fmt_normal |
257 |
| - return fmt.format(name=name, content=content, tagdesc=tagdesc) |
258 |
| - |
259 |
| - def notes(self, item: TagVar): |
260 |
| - for attrib, note in ATTRIB2NOTE.items(): |
261 |
| - if getattr(item, attrib): |
262 |
| - yield html.escape(_(note)) |
263 |
| - |
264 |
| - def related_options_titles(self, item: TagVar): |
265 |
| - if not item.related_options: |
266 |
| - return |
267 |
| - for setting in item.related_options: |
268 |
| - title = get_option_title(setting) |
269 |
| - if title: |
270 |
| - yield html.escape(_(title)) |
271 |
| - |
272 |
| - def links(self, item: TagVar): |
273 |
| - if not item.doc_links: |
274 |
| - return |
275 |
| - for doclink in item.doc_links: |
276 |
| - translated_title = html.escape(_(doclink.title)) |
277 |
| - yield f"<a href='{doclink.link}'>{translated_title}</a>" |
278 |
| - |
279 |
| - def see_alsos(self, item: TagVar): |
280 |
| - if not item.see_also: |
281 |
| - return |
282 |
| - for tag in item.see_also: |
283 |
| - if self.script_name_from_name(tag): |
284 |
| - yield f'<a href="#{tag}">%{tag}%</a>' |
285 |
| - |
286 |
| - def _base_description(self, item: TagVar): |
287 |
| - return _markdown(_(item.longdesc) if item.longdesc else _(TEXT_NO_DESCRIPTION)) |
288 |
| - |
289 |
| - def _add_sections(self, item, include_sections): |
290 |
| - # Note: format has to be translatable, for languages not using left-to-right for example |
291 |
| - fmt = _("<p><strong>{title}:</strong> {values}.</p>") |
292 |
| - return ''.join(self._gen_sections(fmt, item, include_sections)) |
293 |
| - |
294 |
| - def _gen_sections(self, fmt, item, include_sections): |
295 |
| - for section_id in include_sections: |
296 |
| - section = SECTIONS[section_id] |
297 |
| - func_for_values = getattr(self, section.tagvar_func) |
298 |
| - values = tuple(func_for_values(item)) |
299 |
| - if not values: |
300 |
| - continue |
301 |
| - yield fmt.format( |
302 |
| - title=_(section.title), |
303 |
| - values='; '.join(values), |
304 |
| - ) |
305 |
| - |
306 |
| - def tooltip_content(self, item: TagVar): |
307 |
| - content = self._base_description(item) |
308 |
| - content += self._add_sections(item, (Section.notes, )) |
309 |
| - return content |
310 |
| - |
311 |
| - def full_description_content(self, item: TagVar): |
312 |
| - content = self._base_description(item) |
313 |
| - |
314 |
| - # Append additional description |
315 |
| - if item.additionaldesc: |
316 |
| - content += _markdown(_(item.additionaldesc)) |
317 |
| - |
318 |
| - # Append additional sections as required |
319 |
| - include_sections = ( |
320 |
| - Section.notes, |
321 |
| - Section.options, |
322 |
| - Section.links, |
323 |
| - Section.see_also, |
324 |
| - ) |
325 |
| - content += self._add_sections(item, include_sections) |
326 |
| - |
327 |
| - return content |
328 |
| - |
329 |
| - def display_tooltip(self, tagname): |
330 |
| - name, tagdesc, _search_name, item = self.item_from_name(tagname) |
331 |
| - content = self.tooltip_content(item) if item else _markdown(_(TEXT_NO_DESCRIPTION)) |
332 |
| - return self._format_display(name, content, tagdesc) |
333 |
| - |
334 |
| - def display_full_description(self, tagname): |
335 |
| - name, tagdesc, _search_name, item = self.item_from_name(tagname) |
336 |
| - content = self.full_description_content(item) if item else _markdown(_(TEXT_NO_DESCRIPTION)) |
337 |
| - return self._format_display(name, content, tagdesc) |
338 |
| - |
339 |
| - def names(self, selector=None): |
340 |
| - for item in self._items: |
341 |
| - if selector is None or selector(item): |
342 |
| - yield str(item) |
343 |
| - |
344 |
| - |
345 | 31 | ALL_TAGS = TagVars(
|
346 | 32 | TagVar(
|
347 | 33 | 'absolutetracknumber',
|
|
0 commit comments