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