1# This file is part of python-ly, https://pypi.python.org/pypi/python-ly
2#
3# Copyright (c) 2014 - 2015 by Wilbert Berendsen
4#
5# This program is free software; you can redistribute it and/or
6# modify it under the terms of the GNU General Public License
7# as published by the Free Software Foundation; either version 2
8# of the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18# See http://www.gnu.org/licenses/ for more information.
19
20"""
21The commands that are available to the command line.
22"""
23
24from __future__ import unicode_literals
25
26import re
27import sys
28
29import ly
30
31known_commands = [
32    'mode',
33    'version',
34    'language',
35    'indent',
36    'reformat',
37    'translate',
38    'transpose',
39    'rel2abs',
40    'abs2rel',
41    'musicxml',
42    'highlight'
43]
44
45class _command(object):
46    """Base class for commands.
47
48    If the __init__() fails with TypeError or ValueError, the command is
49    considered invalid and an error message will be written to the console
50    in the parse_command() function in main.py.
51
52    By default, __init__() expects no arguments. If your command does accept
53    arguments, they are provided in a single argument that you should parse
54    yourself.
55
56    """
57    def __init__(self):
58        pass
59
60    def run(self, opts, data):
61        pass
62
63
64class set_variable(_command):
65    """set a configuration variable to a value"""
66    def __init__(self, arg):
67        self.name, self.value = arg.split('=', 1)
68
69    def run(self, opts, data):
70        opts.set_variable(self.name, self.value)
71
72
73#################################
74### Base classes for commands ###
75#################################
76
77# The command classes have a run() method that serves as a common structure
78# for each command type.  From within run() a function is called to perform
79# the actual, command-specific funcionality.
80# The run() method also takes care of updating the data object with the correct
81# nesting structure and metadata.
82class _info_command(_command):
83    """
84    Base class for commands that retrieve some info about the document.
85    The result is appended to the data['info'] array as a dict with 'command'
86    and 'info' fields.
87    """
88    def run(self, opts, data):
89        import ly.docinfo
90        info = ly.docinfo.DocInfo(data['doc']['content'].document)
91        text = self.get_info(info)
92        data['info'].append({
93            'command': self.__class__.__name__,
94            'info': text or ""
95        })
96
97    def get_info(self, info):
98        """
99        Should return the desired information from the docinfo object.
100        """
101        raise NotImplementedError()
102
103
104class _edit_command(_command):
105    """
106    Base class for commands that modify the input.
107    The modifications overwrite the content of data['doc']['content'], so a
108    subsequent edit commands builds on the modified content. The command name is
109    added to the data['doc']['commands'] dict so it is possible to retrace which
110    commands have been applied to the final result.
111    """
112    def run(self, opts, data):
113        self.edit(opts, data['doc']['content'])
114        data['doc']['commands'].append(self.__class__.__name__)
115
116    def edit(self, opts, cursor):
117        """Should edit the cursor in-place."""
118        raise NotImplementedError()
119
120
121class _export_command(_command):
122    """
123    Base class for commands that convert the input to another format.
124    For each command an entry will be appended to data['exports'] field of the
125    'data' dict, leaving data['doc'] untouched. Each entry in data['exports']
126    has a ['doc'] and a ['command'] field, allowing the client to identify the
127
128    """
129    def run(self, opts, data):
130        export = self.export(opts, data['doc']['content'], data['exports'])
131        data['exports'].append({
132            'command': self.__class__.__name__,
133            'doc': export
134        })
135
136    def export(self, opts, cursor, exports):
137        """Should return the converted document as string."""
138        raise NotImplementedError()
139
140
141#####################
142### Info commands ###
143#####################
144
145class mode(_info_command):
146    """retrieve mode from document"""
147    def get_info(self, info):
148        return info.mode()
149
150
151class version(_info_command):
152    """retrieve version from document"""
153    def get_info(self, info):
154        return info.version_string()
155
156
157class language(_info_command):
158    """retrieve language from document"""
159    def get_info(self, info):
160        return info.language()
161
162
163#####################
164### Edit commands ###
165#####################
166
167class indent(_edit_command):
168    """run the indenter"""
169    def indenter(self, opts):
170        """Get a ly.indent.Indenter initialized with our options."""
171        import ly.indent
172        i = ly.indent.Indenter()
173        i.indent_tabs = opts.indent_tabs
174        i.indent_width = opts.indent_width
175        return i
176
177    def edit(self, opts, cursor):
178        self.indenter(opts).indent(cursor)
179
180
181class reformat(indent):
182    """reformat the document"""
183    def edit(self, opts, cursor):
184        import ly.reformat
185        ly.reformat.reformat(cursor, self.indenter(opts))
186
187
188class translate(_edit_command):
189    """translate pitch names"""
190    def __init__(self, language):
191        if language not in ly.pitch.pitchInfo:
192            raise ValueError()
193        self.language = language
194
195    def edit(self, opts, cursor):
196        import ly.pitch.translate
197        try:
198            changed = ly.pitch.translate.translate(cursor, self.language, opts.default_language)
199        except ly.pitch.PitchNameNotAvailable as pna:
200            raise ValueError(format(pna))
201        if not changed:
202            version = ly.docinfo.DocInfo(cursor.document).version()
203            ly.pitch.translate.insert_language(cursor.document, self.language, version)
204
205
206class transpose(_edit_command):
207    """transpose music"""
208    def __init__(self, arg):
209        import re
210        result = []
211        for pitch, octave in re.findall(r"([a-z]+)([,']*)", arg):
212            r = ly.pitch.pitchReader("nederlands")(pitch)
213            if r:
214                result.append(ly.pitch.Pitch(*r, octave=ly.pitch.octaveToNum(octave)))
215        self.from_pitch, self.to_pitch = result
216
217    def edit(self, opts, cursor):
218        import ly.pitch.transpose
219        transposer = ly.pitch.transpose.Transposer(self.from_pitch, self.to_pitch)
220        try:
221            ly.pitch.transpose.transpose(cursor, transposer, opts.default_language)
222        except ly.pitch.PitchNameNotAvailable as pna:
223            language = ly.docinfo.DocInfo(cursor.document).language() or opts.default_language
224            raise ValueError(format(pna))
225
226
227class rel2abs(_edit_command):
228    """convert relative music to absolute"""
229    def edit(self, opts, cursor):
230        import ly.pitch.rel2abs
231        ly.pitch.rel2abs.rel2abs(cursor, opts.default_language)
232
233
234class abs2rel(_edit_command):
235    """convert absolute music to relative"""
236    def edit(self, opts, cursor):
237        import ly.pitch.abs2rel
238        ly.pitch.abs2rel.abs2rel(cursor, opts.default_language)
239
240
241#######################
242### Export commands ###
243#######################
244
245class musicxml(_export_command):
246    """convert source to MusicXML"""
247    def export(self, opts, cursor, exports):
248        import ly.musicxml
249        writer = ly.musicxml.writer()
250        writer.parse_document(cursor.document)
251        #TODO!!!
252        # In Python3 this incorrectly escapes the \n characters,
253        # but leaving out the str() conversion returns a Bytes object,
254        # which will in turn trigger an "object is not JSON serializable" error
255        return str(writer.musicxml().tostring())
256
257
258class highlight(_export_command):
259    """convert source to syntax colored HTML."""
260    def export(self, opts, cursor, exports):
261        import ly.colorize
262        w = ly.colorize.HtmlWriter()
263
264        # set configuration options
265        w.full_html = opts.full_html
266        w.inline_style = opts.inline_style
267        w.stylesheet_ref = opts.stylesheet
268        w.number_lines = opts.number_lines
269        w.title = cursor.document.filename
270        w.encoding = opts.output_encoding or "utf-8"
271        w.wrapper_tag = opts.wrapper_tag
272        w.wrapper_attribute = opts.wrapper_attribute
273        w.document_id = opts.document_id
274        w.linenumbers_id = opts.linenumbers_id
275
276        return w.html(cursor)
277