sublime_dsl 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +136 -0
- data/Rakefile +248 -0
- data/SYNTAX.md +927 -0
- data/bin/subdsl +4 -0
- data/lib/sublime_dsl/cli/export.rb +134 -0
- data/lib/sublime_dsl/cli/import.rb +143 -0
- data/lib/sublime_dsl/cli.rb +125 -0
- data/lib/sublime_dsl/core_ext/enumerable.rb +24 -0
- data/lib/sublime_dsl/core_ext/string.rb +129 -0
- data/lib/sublime_dsl/core_ext.rb +4 -0
- data/lib/sublime_dsl/sublime_text/command.rb +157 -0
- data/lib/sublime_dsl/sublime_text/command_set.rb +112 -0
- data/lib/sublime_dsl/sublime_text/keyboard.rb +659 -0
- data/lib/sublime_dsl/sublime_text/keymap/dsl_reader.rb +194 -0
- data/lib/sublime_dsl/sublime_text/keymap.rb +385 -0
- data/lib/sublime_dsl/sublime_text/macro.rb +91 -0
- data/lib/sublime_dsl/sublime_text/menu.rb +237 -0
- data/lib/sublime_dsl/sublime_text/mouse.rb +149 -0
- data/lib/sublime_dsl/sublime_text/mousemap.rb +185 -0
- data/lib/sublime_dsl/sublime_text/package/dsl_reader.rb +91 -0
- data/lib/sublime_dsl/sublime_text/package/exporter.rb +138 -0
- data/lib/sublime_dsl/sublime_text/package/importer.rb +127 -0
- data/lib/sublime_dsl/sublime_text/package/reader.rb +102 -0
- data/lib/sublime_dsl/sublime_text/package/writer.rb +112 -0
- data/lib/sublime_dsl/sublime_text/package.rb +96 -0
- data/lib/sublime_dsl/sublime_text/setting_set.rb +123 -0
- data/lib/sublime_dsl/sublime_text.rb +48 -0
- data/lib/sublime_dsl/textmate/custom_base_name.rb +45 -0
- data/lib/sublime_dsl/textmate/grammar/dsl_reader.rb +383 -0
- data/lib/sublime_dsl/textmate/grammar/dsl_writer.rb +178 -0
- data/lib/sublime_dsl/textmate/grammar/plist_reader.rb +163 -0
- data/lib/sublime_dsl/textmate/grammar/plist_writer.rb +153 -0
- data/lib/sublime_dsl/textmate/grammar.rb +252 -0
- data/lib/sublime_dsl/textmate/plist.rb +141 -0
- data/lib/sublime_dsl/textmate/preference.rb +301 -0
- data/lib/sublime_dsl/textmate/snippet.rb +437 -0
- data/lib/sublime_dsl/textmate/theme/dsl_reader.rb +87 -0
- data/lib/sublime_dsl/textmate/theme/item.rb +74 -0
- data/lib/sublime_dsl/textmate/theme/plist_writer.rb +53 -0
- data/lib/sublime_dsl/textmate/theme.rb +364 -0
- data/lib/sublime_dsl/textmate.rb +9 -0
- data/lib/sublime_dsl/tools/blank_slate.rb +49 -0
- data/lib/sublime_dsl/tools/console.rb +74 -0
- data/lib/sublime_dsl/tools/helpers.rb +152 -0
- data/lib/sublime_dsl/tools/regexp_wannabe.rb +154 -0
- data/lib/sublime_dsl/tools/stable_inspect.rb +20 -0
- data/lib/sublime_dsl/tools/value_equality.rb +37 -0
- data/lib/sublime_dsl/tools/xml.rb +66 -0
- data/lib/sublime_dsl/tools.rb +66 -0
- data/lib/sublime_dsl.rb +23 -0
- metadata +145 -0
data/bin/subdsl
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
module SublimeDSL
|
2
|
+
class CLI
|
3
|
+
|
4
|
+
class Export
|
5
|
+
|
6
|
+
def self.summary
|
7
|
+
"convert a set of DSL source files to a Sublime Text package"
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :main_cmd
|
11
|
+
attr_reader :reader, :exporter
|
12
|
+
|
13
|
+
def initialize(main_cmd)
|
14
|
+
@main_cmd = main_cmd
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
parse_options
|
19
|
+
package = reader.package
|
20
|
+
exit 1 unless package
|
21
|
+
exporter.export package
|
22
|
+
exit
|
23
|
+
rescue OptionError => ex
|
24
|
+
Console.error ex.message
|
25
|
+
Console.error %(type "#{main_cmd.name} help export" for more information)
|
26
|
+
exit 2
|
27
|
+
end
|
28
|
+
|
29
|
+
def error(msg)
|
30
|
+
raise OptionError, msg
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_options
|
34
|
+
package_name = ARGV.shift
|
35
|
+
error 'missing package name' unless package_name
|
36
|
+
@reader = SublimeText::Package::Reader.new(package_name)
|
37
|
+
@exporter = SublimeText::Package::Exporter.new
|
38
|
+
while (option = ARGV.shift)
|
39
|
+
case option
|
40
|
+
when '-s', '--source'
|
41
|
+
path = ARGV.shift
|
42
|
+
path or error "#{option}: missing path"
|
43
|
+
reader.root = path
|
44
|
+
when '-i', '--include'
|
45
|
+
pattern = ARGV.shift
|
46
|
+
pattern or error "#{option}: missing pattern"
|
47
|
+
reader.include = pattern
|
48
|
+
when '-e', '--exclude'
|
49
|
+
pattern = ARGV.shift
|
50
|
+
pattern or error "#{option}: missing pattern"
|
51
|
+
reader.exclude = pattern
|
52
|
+
when '-t', '--target'
|
53
|
+
path = ARGV.shift
|
54
|
+
path or error "#{option}: missing path"
|
55
|
+
exporter.root = path
|
56
|
+
when '-b', '--backup'
|
57
|
+
policy = ARGV.shift
|
58
|
+
case policy
|
59
|
+
when nil
|
60
|
+
policy = :always
|
61
|
+
when /^-/
|
62
|
+
ARGV.unshift policy
|
63
|
+
policy = :always
|
64
|
+
end
|
65
|
+
exporter.backup = policy
|
66
|
+
when '-c', '--cleanup'
|
67
|
+
exporter.cleanup = true
|
68
|
+
when '-q', '--quiet'
|
69
|
+
Console.verbosity = 0
|
70
|
+
when '-V', '--verbose'
|
71
|
+
Console.verbosity = 2
|
72
|
+
else
|
73
|
+
error "invalid argument: #{option.inspect}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
ARGV.empty? or error "invalid arguments: #{ARGV.join(' ')}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def help
|
80
|
+
<<-HELP.dedent
|
81
|
+
Usage: #{main_cmd.name} export <package> [options]
|
82
|
+
|
83
|
+
<package>
|
84
|
+
The name of a directory containing the DSL files to process.
|
85
|
+
|
86
|
+
Options:
|
87
|
+
-s, --source PATH
|
88
|
+
The path to the directory containing the directory <package>.
|
89
|
+
By default, it is the current directory.
|
90
|
+
|
91
|
+
-i, --include PATTERNS
|
92
|
+
Files to process or copy. By default, all files are processed
|
93
|
+
(except the files defined by the --exclude option).
|
94
|
+
PATTERNS must be a list of specifications separated by semicolons,
|
95
|
+
for instance "*.rb;*.py".
|
96
|
+
|
97
|
+
-e, --exclude PATTERNS
|
98
|
+
Files not to process nor copy. By default "*.keyboard.rb"
|
99
|
+
(keyboard definition files are processed when called from
|
100
|
+
keymap definitions).
|
101
|
+
|
102
|
+
-t, --target PATH
|
103
|
+
The path to the target root directory: the generated files
|
104
|
+
will be placed there in a subdirectory named <package>.
|
105
|
+
By default, it is the Sublime Text Packages directory.
|
106
|
+
|
107
|
+
-b, --backup [POLICY]
|
108
|
+
Define the backup policy if the directory <target_path>/<package>
|
109
|
+
already exists and contains files. POLICY may be:
|
110
|
+
never: do not create a backup
|
111
|
+
always: always create a backup
|
112
|
+
once: create a backup if there is not already one
|
113
|
+
Not giving a policy (-b or --backup alone) is the same
|
114
|
+
as 'always'. The default is 'always'.
|
115
|
+
The backup will be a zip file placed in <target_path>,
|
116
|
+
named <package_name>.<time_stamp>.zip
|
117
|
+
|
118
|
+
-c, --cleanup
|
119
|
+
Delete any file in the directory <target_path>/<package>
|
120
|
+
that is not generated by the export process. By default,
|
121
|
+
no file is removed from the target directory.
|
122
|
+
|
123
|
+
-q, --quiet
|
124
|
+
Do not report any information or warning.
|
125
|
+
|
126
|
+
-V, --verbose
|
127
|
+
Report detailed progress information.
|
128
|
+
HELP
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module SublimeDSL
|
2
|
+
class CLI
|
3
|
+
|
4
|
+
class Import
|
5
|
+
|
6
|
+
def self.summary
|
7
|
+
"convert a Sublime Text package to a set of DSL source files"
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :main_cmd
|
11
|
+
attr_reader :importer, :writer
|
12
|
+
|
13
|
+
def initialize(main_cmd)
|
14
|
+
@main_cmd = main_cmd
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
parse_options
|
19
|
+
package = importer.package
|
20
|
+
exit 1 unless package
|
21
|
+
writer.write package
|
22
|
+
exit
|
23
|
+
rescue OptionError => ex
|
24
|
+
Console.error ex.message
|
25
|
+
Console.error %(type "#{main_cmd.name} help import" for more information)
|
26
|
+
exit 2
|
27
|
+
end
|
28
|
+
|
29
|
+
def error(msg)
|
30
|
+
raise OptionError, msg
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_options
|
34
|
+
package_name = ARGV.shift
|
35
|
+
error 'missing package name' unless package_name
|
36
|
+
@importer = SublimeText::Package::Importer.new(package_name)
|
37
|
+
@writer = SublimeText::Package::Writer.new
|
38
|
+
while (option = ARGV.shift)
|
39
|
+
case option
|
40
|
+
when '-s', '--source'
|
41
|
+
path = ARGV.shift
|
42
|
+
path or error "#{option}: missing path"
|
43
|
+
importer.root = path
|
44
|
+
when '-i', '--include'
|
45
|
+
pattern = ARGV.shift
|
46
|
+
pattern or error "#{option}: missing pattern"
|
47
|
+
importer.include = pattern
|
48
|
+
when '-e', '--exclude'
|
49
|
+
pattern = ARGV.shift
|
50
|
+
pattern or error "#{option}: missing pattern"
|
51
|
+
importer.exclude = pattern
|
52
|
+
when '-t', '--target'
|
53
|
+
path = ARGV.shift
|
54
|
+
path or error "#{option}: missing path"
|
55
|
+
writer.root = path
|
56
|
+
when '-k', '--keyboard'
|
57
|
+
keyboard = ARGV.shift
|
58
|
+
keyboard or error "#{option}: missing keyboard"
|
59
|
+
writer.keyboard = keyboard
|
60
|
+
when '-r', '--recognized'
|
61
|
+
writer.copy_other_files = false
|
62
|
+
when '-b', '--backup'
|
63
|
+
policy = ARGV.shift
|
64
|
+
case policy
|
65
|
+
when nil
|
66
|
+
policy = :always
|
67
|
+
when /^-/
|
68
|
+
ARGV.unshift policy
|
69
|
+
policy = :always
|
70
|
+
end
|
71
|
+
writer.backup = policy
|
72
|
+
when '-q', '--quiet'
|
73
|
+
Console.verbosity = 0
|
74
|
+
when '-V', '--verbose'
|
75
|
+
Console.verbosity = 2
|
76
|
+
else
|
77
|
+
error "invalid argument: #{option.inspect}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
ARGV.empty? or error "invalid arguments: #{ARGV.join(' ')}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def help
|
84
|
+
<<-HELP.dedent
|
85
|
+
Usage: #{main_cmd.name} import <package> [options]
|
86
|
+
|
87
|
+
<package>
|
88
|
+
The name of a Sublime Text package directory containing
|
89
|
+
files to convert to DSL files.
|
90
|
+
|
91
|
+
Options:
|
92
|
+
-s, --source PATH
|
93
|
+
The path to the directory containing the directory <package>.
|
94
|
+
By default, it is the Sublime Text Packages directory.
|
95
|
+
|
96
|
+
-i, --include PATTERNS
|
97
|
+
Files to process or copy. By default, all files are processed
|
98
|
+
(except the files defined by the --exclude option).
|
99
|
+
PATTERNS must be a list of specifications separated by semicolons,
|
100
|
+
for instance "*.tmSnippet;*.tmLanguage".
|
101
|
+
|
102
|
+
-e, --exclude PATTERNS
|
103
|
+
Files not to process nor copy. By default "*.cache;*.pyc".
|
104
|
+
|
105
|
+
-t, --target PATH
|
106
|
+
The path to the target root directory: generated DSL files
|
107
|
+
will be placed there in a subdirectory named <package>.
|
108
|
+
By default, it is the current directory.
|
109
|
+
|
110
|
+
-k, --keyboard NAME
|
111
|
+
Convert keymap definitions for keyboard NAME. By default, no
|
112
|
+
keymap conversion is done. A file NAME.keyboard.rb will be
|
113
|
+
searched for in the subdirectories of <target_path>.
|
114
|
+
|
115
|
+
-r, --recognized
|
116
|
+
Do not copy non-recognized files to the target directory.
|
117
|
+
By default, non-recognized files (e.g., Python scripts)
|
118
|
+
are copied "as is" from the source directory to the target
|
119
|
+
directory.
|
120
|
+
|
121
|
+
-b, --backup [POLICY]
|
122
|
+
Define the backup policy if the directory <target_path>/<package>
|
123
|
+
already exists and contains files. POLICY may be:
|
124
|
+
never: do not create a backup
|
125
|
+
always: always create a backup
|
126
|
+
once: create a backup if there is not already one
|
127
|
+
Not giving a policy (-b or --backup alone) is the same
|
128
|
+
as 'always'. The default is 'once'.
|
129
|
+
The backup will be a zip file placed in <target_path>,
|
130
|
+
named <package_name>.<time_stamp>.zip
|
131
|
+
|
132
|
+
-q, --quiet
|
133
|
+
Do not report any information or warning.
|
134
|
+
|
135
|
+
-V, --verbose
|
136
|
+
Report detailed progress information.
|
137
|
+
HELP
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module SublimeDSL
|
4
|
+
|
5
|
+
##
|
6
|
+
# A Command Line Interpreter.
|
7
|
+
#
|
8
|
+
# Exit codes:
|
9
|
+
# * 0: command successful
|
10
|
+
# * 1: command did nothing
|
11
|
+
# * 2: something went wrong
|
12
|
+
|
13
|
+
class CLI
|
14
|
+
|
15
|
+
class << self
|
16
|
+
|
17
|
+
# Returns a Hash { command_name => command_class }.
|
18
|
+
def cmd_class_hash
|
19
|
+
@cmd_class_hash ||= load_commands
|
20
|
+
end
|
21
|
+
|
22
|
+
def load_commands
|
23
|
+
h = {}
|
24
|
+
Dir.chdir File.dirname(__FILE__) do
|
25
|
+
Dir["cli/*.rb"].each do |f|
|
26
|
+
require_relative f.sub(/\.rb$/, '')
|
27
|
+
c = File.basename(f, '.rb')
|
28
|
+
h[c] = CLI.const_get(c.pascal_case)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
h
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :name
|
37
|
+
|
38
|
+
def initialize(name)
|
39
|
+
@name = name
|
40
|
+
end
|
41
|
+
|
42
|
+
def run
|
43
|
+
|
44
|
+
# honor help & version
|
45
|
+
while ARGV.first && ARGV.first.start_with?('-')
|
46
|
+
option = ARGV.shift
|
47
|
+
case option
|
48
|
+
when '-h', '--help'
|
49
|
+
puts help
|
50
|
+
exit 1
|
51
|
+
when '-v', '--version'
|
52
|
+
puts version
|
53
|
+
exit 1
|
54
|
+
else
|
55
|
+
Console.error "invalid option: #{option.inspect}"
|
56
|
+
exit 2
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
cmd_name = ARGV.shift
|
61
|
+
|
62
|
+
# if no argument, print help & exit
|
63
|
+
unless cmd_name
|
64
|
+
puts help
|
65
|
+
exit 2
|
66
|
+
end
|
67
|
+
|
68
|
+
if cmd_name == 'help'
|
69
|
+
if ARGV.empty?
|
70
|
+
puts help
|
71
|
+
else
|
72
|
+
arg = ARGV.shift
|
73
|
+
if arg == 'commands'
|
74
|
+
puts "Available commands:"
|
75
|
+
CLI.cmd_class_hash.each do |name, klass|
|
76
|
+
puts " %-10s %s" % [name, klass.summary]
|
77
|
+
end
|
78
|
+
else
|
79
|
+
puts command(arg).help
|
80
|
+
end
|
81
|
+
end
|
82
|
+
exit 1
|
83
|
+
end
|
84
|
+
|
85
|
+
command(cmd_name).run
|
86
|
+
end
|
87
|
+
|
88
|
+
def command(name)
|
89
|
+
cmd_class = CLI.cmd_class_hash[name]
|
90
|
+
unless cmd_class
|
91
|
+
Console.error "invalid command #{name.inspect}\n" \
|
92
|
+
"valid commands: #{CLI.cmd_class_hash.keys.join(', ')}"
|
93
|
+
exit 2
|
94
|
+
end
|
95
|
+
|
96
|
+
cmd_class.new(self)
|
97
|
+
end
|
98
|
+
|
99
|
+
def help
|
100
|
+
<<-HELP.dedent
|
101
|
+
Sublime DSL allows defining Sublime Text packages using a Ruby DSL.
|
102
|
+
|
103
|
+
Usage:
|
104
|
+
#{name} -h/--help
|
105
|
+
#{name} -v/--version
|
106
|
+
#{name} <command> [arguments...] [options...]
|
107
|
+
|
108
|
+
Examples:
|
109
|
+
#{name} import Ruby
|
110
|
+
#{name} export Ruby --cleanup
|
111
|
+
|
112
|
+
Available commands:
|
113
|
+
import import to DSL
|
114
|
+
export export from DSL
|
115
|
+
help <command> show help on <command>
|
116
|
+
HELP
|
117
|
+
end
|
118
|
+
|
119
|
+
def version
|
120
|
+
"SublimeDSL #{SublimeDSL::VERSION}"
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Enumerable
|
4
|
+
|
5
|
+
# Returns a hash <tt>{ key => element }</tt>, where +key+ is the
|
6
|
+
# value returned by the block when passed +element+.
|
7
|
+
#
|
8
|
+
# Raises an exception if two elements return the same key.
|
9
|
+
#
|
10
|
+
# Examples:
|
11
|
+
# %w(a bb ccc).keyed_by(&:length) #=> {1 => 'a', 2 => 'bb', 3 => 'ccc'}
|
12
|
+
# %w(a bb cc).keyed_by(&:length) #=> RuntimeError "duplicate key: 2"
|
13
|
+
|
14
|
+
def keyed_by
|
15
|
+
h = {}
|
16
|
+
each do |element|
|
17
|
+
key = yield(element)
|
18
|
+
h.key?(key) and raise "duplicate key: #{key.inspect}"
|
19
|
+
h[key] = element
|
20
|
+
end
|
21
|
+
h
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class String
|
4
|
+
|
5
|
+
# Same as #inspect, but between single quotes,
|
6
|
+
# so only \ and single quotes are escaped.
|
7
|
+
def inspect_sq
|
8
|
+
# escape all \ trailing, or followed by a \ or a '
|
9
|
+
# escape single quotes
|
10
|
+
"'" << self.gsub(/\\$|\\(?=[\\'])/, '\\\\\\\\').gsub("'", "\\\\'") << "'"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Same as #inspect, but only escapes control characters and double quotes.
|
14
|
+
def inspect_dq
|
15
|
+
'"' << self.gsub(/[[:cntrl:]\\"]/) { |c| c.inspect[1..-2] } << '"'
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns the shortest between #inspect_sq and #inspect_dq.
|
19
|
+
# If same length, acts according to +prefer_dq+
|
20
|
+
# (prefer double quotes?)
|
21
|
+
def to_source(prefer_dq = false)
|
22
|
+
sq = inspect_sq
|
23
|
+
dq = inspect_dq
|
24
|
+
case dq.length <=> sq.length
|
25
|
+
when 1 then sq
|
26
|
+
when -1 then dq
|
27
|
+
when 0 then prefer_dq ? dq : sq
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
HTML_ESCAPE_MAP = { '&' => '&', '"' => '"', '<' => '<', '>' => '>' }
|
32
|
+
|
33
|
+
# Returns the string with characters escaped to HTML entities:
|
34
|
+
# < => <
|
35
|
+
# > => >
|
36
|
+
# & => &
|
37
|
+
# " => " (if escape_quotes == true)
|
38
|
+
def html_escape(escape_quotes = true)
|
39
|
+
if escape_quotes
|
40
|
+
self.gsub(/[&><"]/, HTML_ESCAPE_MAP)
|
41
|
+
else
|
42
|
+
self.gsub(/[&><]/, HTML_ESCAPE_MAP)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# <tt>pascal_case('snake_case') # => SnakeCase</tt>.
|
47
|
+
def pascal_case
|
48
|
+
self.gsub(/(?:^|_)(.)/) { $1.upcase }
|
49
|
+
end
|
50
|
+
|
51
|
+
# <tt>camel_case('snake_case') # => snakeCase</tt>.
|
52
|
+
def camel_case
|
53
|
+
self.gsub(/_(.)/) { $1.upcase }
|
54
|
+
end
|
55
|
+
|
56
|
+
# <tt>snake_case('PascalCase') # => pascal_case</tt>.
|
57
|
+
# <tt>snake_case('camelCase') # => camel_case</tt>.
|
58
|
+
def snake_case
|
59
|
+
self
|
60
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
61
|
+
.gsub(/([a-z\d])([A-Z])/,'\1_\2')
|
62
|
+
.downcase
|
63
|
+
end
|
64
|
+
|
65
|
+
# Shifts all lines left, preserving relative indentation.
|
66
|
+
def dedent
|
67
|
+
self.dup.dedent!
|
68
|
+
end
|
69
|
+
|
70
|
+
# Shifts all lines left, preserving relative indentation.
|
71
|
+
# Returns +self+.
|
72
|
+
def dedent!
|
73
|
+
if (re = dedent_regexp)
|
74
|
+
self.gsub! re, ''
|
75
|
+
end
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
# Indents all non-empty lines by the number of spaces passed.
|
80
|
+
def indent(spaces)
|
81
|
+
self.gsub(/^(.+)/, (' ' * spaces) + '\1')
|
82
|
+
end
|
83
|
+
|
84
|
+
# Indents all non-empty lines by the number of spaces passed.
|
85
|
+
# Returns +self+.
|
86
|
+
def indent!(spaces)
|
87
|
+
self.gsub!(/^(.+)/, (' ' * spaces) + '\1')
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
# Wraps at spaces so that no line is longer than the passed +width+,
|
92
|
+
# if possible. Multiple newlines, tabs & spaces are replaced by
|
93
|
+
# a single space.
|
94
|
+
def wrap(width = 80)
|
95
|
+
words = self.split(/\s+/m)
|
96
|
+
wrapped = words.shift
|
97
|
+
len = wrapped.length
|
98
|
+
words.each do |w|
|
99
|
+
len += 1 + w.length
|
100
|
+
if len > width
|
101
|
+
wrapped << "\n" << w
|
102
|
+
len = w.length
|
103
|
+
else
|
104
|
+
wrapped << ' ' << w
|
105
|
+
end
|
106
|
+
end
|
107
|
+
wrapped
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
# Returns the regexp to dedent +self+, or +nil+ if no dedent is needed,
|
113
|
+
# or there are leading tabs on non-empty lines.
|
114
|
+
def dedent_regexp
|
115
|
+
min_indent = self.length
|
116
|
+
self.each_line do |line|
|
117
|
+
# if no leading space, no dedent
|
118
|
+
line =~ /^\S/ and return nil
|
119
|
+
# skip empty lines
|
120
|
+
line =~ /^\s*$/ and next
|
121
|
+
# do nothing when tabs are present
|
122
|
+
line =~ /^ *\t/ and return nil
|
123
|
+
indent = line.match(/^ +/).to_s.length
|
124
|
+
min_indent = indent if indent < min_indent
|
125
|
+
end
|
126
|
+
Regexp.new('^' << ' ' * min_indent, Regexp::MULTILINE)
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|