1
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
3
# Copyright (c) 2008, 2009, 2010 by Wilbert Berendsen
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.
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.
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.
20
from __future__ import unicode_literals
23
Indent LilyPond input.
25
Recognizes common LilyPond mode and Scheme mode.
27
This module is not dependent on any other module,
28
besides the Python standard re module.
33
# tokens to look for in LilyPond mode
37
r'|(?P<string>"(\\[\\"]|[^"])*")'
38
r"|(?P<newline>\n[^\S\n]*)"
39
r"|(?P<space>[^\S\n]+)"
41
r"|(?P<blockcomment>%\{.*?%\})"
42
r"|(?P<longcomment>%%%[^\n]*)"
43
r"|(?P<comment>%[^\n]*)"
46
# tokens to look for in Scheme mode
50
r'|(?P<string>"(\\[\\"]|[^"])*")'
51
r"|(?P<newline>\n[^\S\n]*)"
52
r"|(?P<space>[^\S\n]+)"
54
r"|(?P<longcomment>;;;[^\n]*)"
55
r"|(?P<blockcomment>#!.*?!#)"
56
r"|(?P<comment>;[^\n]*)"
59
# tokens to look for in LilyPond-inside-Scheme mode
60
schemelily_re = r"(?P<backtoscheme>#\})|" + lily_re
64
lily = re.compile(lily_re, re.DOTALL)
66
# Parse LilyPond-in-Scheme text
67
schemelily = re.compile(schemelily_re, re.DOTALL)
69
# Parse Scheme text, instantiate to keep state (depth)
71
search = re.compile(scheme_re, re.DOTALL).search
75
# searches for indent inside a string
76
indent_rx = re.compile(r'\n([^\S\n]*)')
87
Properly indents the LilyPond input in text.
89
If start is an integer value, use that value as the indentwidth to start
90
with, disregarding the current indent of the first line.
91
If it is None, use the indent of the first line.
93
indentwidth: how many positions to indent (default 2)
94
tabwidth: width of a tab character
95
usetabs: whether to use tab characters in the indent:
96
- None = determine from document
97
- True = use tabs for the parts of the indent that exceed the tab width
98
- False = don't use tabs.
99
startscheme: start in scheme mode (not very robust)
102
# record length of indent of first line
103
space = re.match(r'[^\S\n]*', text).group()
105
start = len(space.expandtabs(tabwidth))
107
usetabs = '\t' in space or '\n\t' in text
109
mode = [lily] # the mode to parse in
110
indent = [start] # stack with indent history
111
pos = len(space) # start position in text
112
output = [] # list of output lines
115
mode.append(scheme())
117
makeindent = lambda i: '\t' * int(i / tabwidth) + ' ' * (i % tabwidth)
119
makeindent = lambda i: ' ' * i
121
line = [] # list to build the output, per line
122
curindent = -1 # current indent in count of spaces, -1 : not yet set
124
# Search the text from the previous position
125
# (very fast: does not alter the string in text)
126
m = mode[-1].search(text, pos)
128
# also append text before the found token
129
more = pos < m.start()
131
line.append(text[pos:m.start()])
133
# type, text, and new position for next search
134
item, token, pos = m.lastgroup, m.group(), m.end()
136
# If indent not yet determined, set it to 0 if we found a long comment
137
# (with three or more %%% or ;;; characters). Was any other text found,
138
# keep the current indent level for the current line.
139
# (Our current indent can change if our line starts with dedent tokens.)
141
if item == 'longcomment':
143
elif (more or item not in ('dedent', 'space', 'backtoscheme')):
144
curindent = indent[-1]
146
# Check if we found a multiline block comment.
147
# Thoses are handled specially. Indents inside the block comment are
148
# preserved but positioned as close as possible to the current indent.
149
# So the algorithm cuts the shortest indent off from all lines and then
150
# adds the current indent.
151
if item == 'blockcomment' and '\n' in token:
152
# Find the shortest indent inside the block comment
153
shortest = min(len(n.group(1).expandtabs(tabwidth))
154
for n in indent_rx.finditer(token))
155
# Remove that indent from all lines
156
fixindent = lambda n: '\n' + makeindent(
157
curindent - shortest + len(n.group(1).expandtabs(tabwidth)))
158
token = indent_rx.sub(fixindent, token)
160
elif mode[-1] in (lily, schemelily):
161
# we are parsing in LilyPond mode.
163
indent.append(indent[-1] + indentwidth)
164
elif item == 'dedent' and len(indent) > 1:
166
elif item == 'scheme':
167
mode.append(scheme()) # enter scheme mode
168
elif item == 'backtoscheme':
170
mode.pop() # leave lilypond mode, back to scheme
172
# we are parsing in Scheme mode.
174
mode[-1].depth += 1 # count parentheses
175
# look max 10 characters ahead to vertically align opening
176
# parentheses, but stop at closing parenthesis, quote or newline.
177
n = re.search(r'[()"\n]', text[pos:pos+10])
178
if n and n.group() == '(':
179
indent.append(indent[-1] + n.start() + 1)
181
indent.append(indent[-1] + indentwidth)
183
elif item == 'dedent':
186
if mode[-1].depth <= 1:
187
mode.pop() # leave scheme mode
189
mode[-1].depth -= 1 # count parentheses backwards
190
elif item == 'lilypond':
191
mode.append(schemelily) # enter lilypond-in-scheme mode
192
indent.append(indent[-1] + indentwidth)
193
elif mode[-1].depth == 0:
194
# jump out if we got one atom or are at a space or end of line
195
# and still no opening parenthesis. But stay if we only just
197
if (item in ('string', 'comment', 'longcomment')
198
or (more and item in ('newline', 'space'))):
201
if item == 'newline':
203
output.append(makeindent(curindent) + ''.join(line))
209
# On to the next token
210
m = mode[-1].search(text, pos)
212
# Still some text left?
214
line.append(text[pos:])
217
curindent = indent[-1]
218
output.append(makeindent(curindent) + ''.join(line))
220
output.append(makeindent(start))
221
# Return formatted output
222
return '\n'.join(output)
225
if __name__ == '__main__':
229
op = optparse.OptionParser(usage='usage: %prog [options] [filename]')
230
op.add_option('-o', '--output',
231
help='write to this file instead of standard output')
232
op.add_option('-i', '--indent-width', type='int', default=2,
233
help='indent width in characters to use [default: %default]')
234
op.add_option('-t', '--tab-width', type='int', default=8,
235
help='tab width to assume [default: %default]')
236
op.add_option('-s', '--start-indent', type='int', default=0,
237
help='start indent [default: %default]')
238
op.add_option('--scheme', action='store_true',
239
help='start indenting in Scheme mode')
240
op.add_option('-u', '--use-tabs', action='store_true',
241
help='use tabs instead of spaces for indent')
242
options, args = op.parse_args()
244
# TODO: error handling
245
infile = args and open(args[0]) or sys.stdin
248
start=options.start_indent,
249
indentwidth=options.indent_width,
250
tabwidth=options.tab_width,
251
usetabs=options.use_tabs,
252
startscheme=options.scheme
254
outfile = options.output and (options.output) or sys.stdout
257
if infile is not sys.stdin:
259
if outfile is not sys.stdout: