├── Support
├── working_copy.rb
├── cvs_diff.rb
├── diff.rb
├── Stylesheets
│ └── cvs_style.css
├── versioned_file.rb
├── cvs_commit.rb
└── lib
│ └── Builder.rb
├── README.mdown
├── Commands
├── Log.plist
├── Annotate.plist
├── Revert.plist
├── Commit....plist
├── Diff With Newest (HEAD).plist
├── Diff With Working Copy (BASE).plist
├── Add to Repository.plist
├── Diff With Previous Revision (PREV).plist
├── Reset sticky tags.plist
├── Annotate Line.plist
├── Update with tag....plist
├── Merge from tag....plist
├── Check out Revision....plist
└── Diff With Revision....plist
└── info.plist
/Support/working_copy.rb:
--------------------------------------------------------------------------------
1 | module CVS
2 | class WorkingCopy < VersionedFile
3 | def initialize(path)
4 | @path = (path =~ %r{/$}) ? path : "#{path}/" # add / if not there
5 | end
6 |
7 | def dirname
8 | @path
9 | end
10 |
11 | def basename
12 | '.'
13 | end
14 |
15 | def status
16 | cvs(:update, :pretend => true, :quiet => true).inject({}) do |files,line|
17 | files.update($1 => status_from_line(line)) if line =~ /^\S (.*)$/
18 | files
19 | end
20 | end
21 | end
22 | end
--------------------------------------------------------------------------------
/Support/cvs_diff.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby18 -w
2 | # encoding: utf-8
3 |
4 | $LOAD_PATH << ENV['TM_SUPPORT_PATH'] + "/lib"
5 | require 'progress'
6 | require 'versioned_file'
7 |
8 | module CVS
9 | def CVS.diff_active_file(revision, command)
10 | target_path = ENV['TM_FILEPATH']
11 | output_path = File.basename(target_path) + ".diff"
12 |
13 | TextMate::call_with_progress(:title => command,
14 | :message => "Accessing CVS Repository…",
15 | :output_filepath => output_path) do
16 | have_data = false
17 |
18 | # idea here is to stream the data rather than submit it in one big block
19 | VersionedFile.diff(target_path, revision).each_line do |line|
20 | have_data = true unless line.empty?
21 | puts line
22 | end
23 |
24 | if not have_data then
25 | # switch to tooltip output to report lack of differences
26 | puts "No differences found."
27 | exit 206;
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/README.mdown:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | You can install this bundle in TextMate by opening the preferences and going to the bundles tab. After installation it will be automatically updated for you.
4 |
5 | # General
6 |
7 | * [Bundle Styleguide](http://kb.textmate.org/bundle_styleguide) — _before you make changes_
8 | * [Commit Styleguide](http://kb.textmate.org/commit_styleguide) — _before you send a pull request_
9 | * [Writing Bug Reports](http://kb.textmate.org/writing_bug_reports) — _before you report an issue_
10 |
11 | # License
12 |
13 | If not otherwise specified (see below), files in this repository fall under the following license:
14 |
15 | Permission to copy, use, modify, sell and distribute this
16 | software is granted. This software is provided "as is" without
17 | express or implied warranty, and with no claim as to its
18 | suitability for any purpose.
19 |
20 | An exception is made for files in readable text which contain their own license information, or files where an accompanying file exists (in the same directory) with a “-license” suffix added to the base-name name of the original file, and an extension of txt, html, or similar. For example “tidy” is accompanied by “tidy-license.txt”.
--------------------------------------------------------------------------------
/Commands/Log.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
*/
21 | pre {
22 | word-wrap: break-word;
23 | }
24 |
25 | /* for error formating.. */
26 | div.error {
27 | font-family: "Bitstream Vera Sans Mono", monospace;
28 | font-size: 12px;
29 | color: #f30;
30 | background-color: #fee;
31 | border: 2px solid #f52;
32 | padding: 4px;
33 | margin: 3px;
34 | }
35 |
36 | div.error h2 {
37 | font-family: "Lucida Grande", sans-serif;
38 | font-size: 17px;
39 | margin-top: 0;
40 | }
41 |
42 | div.error a:link, div.error a:visited {
43 | background-color: transparent;
44 | color: inherit;
45 | }
46 | div.error a:hover, div.error a:active {
47 | background-color: #f52;
48 | text-decoration: none;
49 | color: #fee;
50 | }
51 |
52 |
53 | /* for showing the path or other information about the current operation */
54 | div.command {
55 | color: #03f;
56 | background-color: #eef;
57 | border: 2px solid #25f;
58 | padding: 4px;
59 | margin: 3px;
60 | font-family: "Bitstream Vera Sans Mono", monospace;
61 | font-size: 10px;
62 | }
63 |
64 | div.command h2 {
65 | font-family: "Lucida Grande", sans-serif;
66 | font-size: 15px;
67 | margin-top: 0;
68 | }
69 |
70 |
71 | /* about links.. */
72 | a:link, a:visited {
73 | background-color: transparent;
74 | color: #35a;
75 | text-decoration: underline;
76 | padding-left: 1px;
77 | padding-right: 1px;
78 | }
79 |
80 | a:hover, a:active {
81 | background-color: #136;
82 | color: #fff;
83 | text-decoration: none;
84 | }
85 |
86 |
87 | /* for everything alternating: */
88 | .alternate {
89 | background-color: #f2f2f2;
90 | }
91 |
--------------------------------------------------------------------------------
/Commands/Check out Revision....plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env bash
9 | [[ -f "${TM_SUPPORT_PATH}/lib/bash_init.sh" ]] && . "${TM_SUPPORT_PATH}/lib/bash_init.sh"
10 |
11 | cd "$TM_DIRECTORY"
12 | revs=$("${TM_CVS:=cvs}" log "$TM_FILENAME"|grep '^revision' \
13 | 2> >(CocoaDialog progressbar --indeterminate \
14 | --title "Check out Revision…" \
15 | --text "Retrieving List of Revisions…" \
16 | ))
17 |
18 | revs=$(echo $revs|sed 's/revision //g')
19 |
20 | revs=`osascript<<END
21 | set AppleScript's text item delimiters to {" "}
22 | tell app "SystemUIServer"
23 | activate
24 | set ourList to (every text item of "$revs")
25 | if the count of items in ourList is 0 then
26 | display dialog "No older revisions of file '$(basename "$TM_FILEPATH")' found" buttons ("OK")
27 | set the result to false
28 | else
29 | choose from list ourList with prompt "Check out an older revision of '$(basename "$TM_FILEPATH")':"
30 | end if
31 | end tell
32 | END`
33 |
34 | # exit if user canceled
35 | if [[ $revs = "false" ]]; then
36 | exit_discard
37 | fi
38 |
39 | export REVS="$revs"
40 | ruby18 <<'END'
41 |
42 | ENV['CVS_PATH'] = ENV['TM_CVS']
43 | $LOAD_PATH << ENV['TM_BUNDLE_SUPPORT']
44 | require 'versioned_file'
45 |
46 | print CVS::VersionedFile.version(ENV['TM_FILEPATH'], ENV['REVS'])
47 | END
48 | input
49 | none
50 | inputFormat
51 | text
52 | keyEquivalent
53 | ^Z
54 | name
55 | Check out Revision...
56 | outputCaret
57 | interpolateByLine
58 | outputFormat
59 | text
60 | outputLocation
61 | replaceDocument
62 | requiredCommands
63 |
64 |
65 | command
66 | cvs
67 | locations
68 |
69 | /opt/local/bin/cvs
70 | /usr/local/bin/cvs
71 |
72 | variable
73 | TM_CVS
74 |
75 |
76 | uuid
77 | 2C5DB599-04DC-40CC-BBE8-0A73620BC42A
78 | version
79 | 2
80 |
81 |
82 |
--------------------------------------------------------------------------------
/Commands/Diff With Revision....plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env bash
9 | [[ -f "${TM_SUPPORT_PATH}/lib/bash_init.sh" ]] && . "${TM_SUPPORT_PATH}/lib/bash_init.sh"
10 |
11 | cd "$TM_DIRECTORY"
12 | revs=$("${TM_CVS:=cvs}" log "$TM_FILENAME"|grep '^revision' \
13 | 2> >(CocoaDialog progressbar --indeterminate \
14 | --title "Diff Revision…" \
15 | --text "Retrieving List of Revisions…" \
16 | ))
17 |
18 | revs=$(echo $revs|sed 's/revision //g')
19 |
20 | revs=`osascript<<END
21 | set AppleScript's text item delimiters to {" "}
22 | tell app "SystemUIServer"
23 | activate
24 | set ourList to (every text item of "$revs")
25 | if the count of items in ourList is 0 then
26 | display dialog "No older revisions of file '$(basename "$TM_FILEPATH")' found" buttons ("OK")
27 | set the result to false
28 | else
29 | choose from list ourList with prompt "Diff '$(basename "$TM_FILEPATH")' with older revision:"
30 | end if
31 | end tell
32 | END`
33 |
34 | # exit if user canceled
35 | if [[ $revs = "false" ]]; then
36 | osascript -e 'tell app "TextMate" to activate' &>/dev/null & exit_discard
37 | fi
38 |
39 | export REVS="$revs"
40 | ruby18 <<'END'
41 |
42 | ENV['CVS_PATH'] = ENV['TM_CVS']
43 | $LOAD_PATH << ENV['TM_BUNDLE_SUPPORT']
44 | require 'cvs_diff'
45 |
46 | CVS::diff_active_file(ENV['REVS'], "Diff With Revision...")
47 | END
48 | input
49 | none
50 | inputFormat
51 | text
52 | keyEquivalent
53 | ^Z
54 | name
55 | Diff With Revision...
56 | outputCaret
57 | afterOutput
58 | outputFormat
59 | text
60 | outputLocation
61 | newWindow
62 | requiredCommands
63 |
64 |
65 | command
66 | cvs
67 | locations
68 |
69 | /opt/local/bin/cvs
70 | /usr/local/bin/cvs
71 |
72 | variable
73 | TM_CVS
74 |
75 |
76 | uuid
77 | 6416A49F-8B3E-47EE-81B4-F2F7F19C6B41
78 | version
79 | 2
80 |
81 |
82 |
--------------------------------------------------------------------------------
/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | contactEmailRot13
6 | oevna.qbabina@tznvy.pbz
7 | contactName
8 | Brian Donovan
9 | description
10 | This bundle gives you easy access to most functions of the <a href="http://www.nongnu.org/cvs/">CVS revision control system</a>.
11 | mainMenu
12 |
13 | items
14 |
15 | ADCD4FCD-D39D-41B3-88D0-84C5BE115535
16 | 2C5DB599-04DC-40CC-BBE8-0A73620BC42A
17 | BE6728A5-AFC4-4D98-9EC7-C2E951483B71
18 | 52464886-2584-4632-A105-12E3A9E6051F
19 | 2BABA244-2BB2-4F3C-BA72-66ADEA8FAA01
20 | D2411BE8-CF0D-4F61-A51F-9587F267D6D0
21 | 20865252-80D2-4CA4-9834-391D09210C4F
22 |
23 | submenus
24 |
25 | 2BABA244-2BB2-4F3C-BA72-66ADEA8FAA01
26 |
27 | items
28 |
29 | 338A3670-DA8E-4036-87E0-DF2E212254C8
30 | 76E34DE2-1DCB-47B8-BA2F-4F3341A3AB9C
31 | 9EA691A5-A166-4D8F-955F-270490F02827
32 |
33 | name
34 | History
35 |
36 | 52464886-2584-4632-A105-12E3A9E6051F
37 |
38 | items
39 |
40 | 22FC4CAB-4664-4CFC-BC8E-C2294616E464
41 | 00C541DE-9A5C-4C59-A075-E754BAEB25C2
42 | E29C9E3B-B7FB-4ED1-94C3-2F702CD090B5
43 | 6416A49F-8B3E-47EE-81B4-F2F7F19C6B41
44 |
45 | name
46 | Diff
47 |
48 | D2411BE8-CF0D-4F61-A51F-9587F267D6D0
49 |
50 | items
51 |
52 | 473C6519-F164-4496-A699-F9DE2CAB56DD
53 | 1F22884A-6702-4FB6-B4E7-D49B2431BD4E
54 | 1FE7E10E-70B4-44D7-924D-879C54F19289
55 |
56 | name
57 | Tags
58 |
59 |
60 |
61 | name
62 | CVS
63 | ordering
64 |
65 | ADCD4FCD-D39D-41B3-88D0-84C5BE115535
66 | 338A3670-DA8E-4036-87E0-DF2E212254C8
67 | 76E34DE2-1DCB-47B8-BA2F-4F3341A3AB9C
68 | 2C5DB599-04DC-40CC-BBE8-0A73620BC42A
69 | BE6728A5-AFC4-4D98-9EC7-C2E951483B71
70 | 22FC4CAB-4664-4CFC-BC8E-C2294616E464
71 | 00C541DE-9A5C-4C59-A075-E754BAEB25C2
72 | E29C9E3B-B7FB-4ED1-94C3-2F702CD090B5
73 | 6416A49F-8B3E-47EE-81B4-F2F7F19C6B41
74 | 9EA691A5-A166-4D8F-955F-270490F02827
75 | 20865252-80D2-4CA4-9834-391D09210C4F
76 | 1FE7E10E-70B4-44D7-924D-879C54F19289
77 | 473C6519-F164-4496-A699-F9DE2CAB56DD
78 | 1F22884A-6702-4FB6-B4E7-D49B2431BD4E
79 |
80 | uuid
81 | 62FB4173-A31D-41D6-8201-16C7866F567E
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Support/versioned_file.rb:
--------------------------------------------------------------------------------
1 | module CVS
2 | CVS_PATH = ENV['CVS_PATH'] || 'cvs' unless defined?(CVS_PATH)
3 |
4 | class VersionedFile
5 | attr_accessor :path
6 |
7 | def initialize(path)
8 | @path = path
9 | end
10 |
11 | def dirname
12 | (File.dirname(@path) =~ %r{/$}) ? File.dirname(@path) : "#{File.dirname(@path)}/"
13 | end
14 |
15 | def basename
16 | File.basename @path
17 | end
18 |
19 | def status
20 | status_from_line cvs(:update, :pretend => true, :quiet => true)
21 | end
22 |
23 | def revision
24 | $1 if cvs(:status) =~ /Working revision:\s*([\d\.]+)/
25 | end
26 |
27 | def revisions(reload = false)
28 | @revisions = nil if reload
29 | @revisions ||= cvs(:log).inject([]) { |list,line| list << $1 if line =~ /^revision ([\d\.]+)/i; list}
30 | end
31 |
32 | def update(options={})
33 | options = options.dup
34 | if options.key?(:tag)
35 | options[:tag] = expand_revision(options[:tag])
36 | options[:sticky] = true unless options.key?(:sticky)
37 | options[:command_options] = "#{options[:sticky] ? '-r' : '-j'} #{options[:tag]}"
38 | elsif options[:reset_tags]
39 | options[:command_options] = '-A'
40 | end
41 | cvs(:update, options)
42 | end
43 |
44 | def diff(revision, other_revision = nil)
45 | revision, other_revision = expand_revision(revision), expand_revision(other_revision)
46 |
47 | if other_revision
48 | cvs(:diff, "-r #{other_revision} -r #{revision}")
49 | else
50 | cvs(:diff, "-r #{revision}")
51 | end
52 | end
53 |
54 | def version(revision)
55 | cvs(:update, "-p -r #{expand_revision(revision)}")
56 | end
57 |
58 | def commit(options={})
59 | options = options.dup
60 | options[:command_options] = "-m '#{options.delete(:message).gsub(/'/, "\\'")}'" if options.key?(:message)
61 | cvs(:commit, options)
62 | end
63 |
64 | def cvs(command, options={})
65 | options = {:command_options => options} if options.is_a? String
66 | cvs_options = [options[:cvs_options]].flatten.compact
67 | cvs_options << '-n' if options[:pretend]
68 | cvs_options << '-q' if options[:quiet]
69 | cvs_options << '-Q' if options[:silent]
70 | cvs_options = cvs_options.join(' ')
71 |
72 | files = options[:files] || [basename]
73 | files = files.map { |file| %("#{file.gsub(/"/, '\\"')}") }.join(' ')
74 | %x{cd "#{dirname}"; "#{CVS_PATH}" #{cvs_options} #{command} #{options[:command_options]} #{files} 2> /dev/null}
75 | end
76 |
77 | %w(status revisions diff revision version cvs).each do |method|
78 | class_eval "def self.#{method}(path, *args); new(path).#{method}(*args); end"
79 | end
80 |
81 | protected
82 |
83 | def expand_revision(revision)
84 | case revision
85 | when :head then 'HEAD'
86 | when :base then 'BASE'
87 | when :prev then revisions[revisions.index(self.revision)+1] rescue nil
88 | else revision
89 | end
90 | end
91 |
92 | def status_from_line(line)
93 | case line
94 | when /^(U|P) /i then :stale
95 | when /^A /i then :added
96 | when /^M /i then :modified
97 | when /^C /i then :conflicted
98 | when /^\? /i then :unknown
99 | when /^R /i then :removed
100 | else :current
101 | end
102 | end
103 | end
104 | end
--------------------------------------------------------------------------------
/Support/cvs_commit.rb:
--------------------------------------------------------------------------------
1 | require 'English' # you are angry, english!
2 |
3 | cvs = ENV['TM_CVS'] || 'cvs' unless defined?(TM_CVS)
4 | #commit_paths = ENV['CommitPaths']
5 | commit_tool = ENV['CommitWindow']
6 | bundle = ENV['TM_BUNDLE_SUPPORT']
7 | support = ENV['TM_SUPPORT_PATH']
8 | ignore_file_pattern = /(\/.*)*(\/\..*|\.(tmproj|o|pyc)|Icon)/
9 |
10 | CURRENT_DIR = Dir.pwd + "/"
11 |
12 | require (bundle + '/versioned_file.rb')
13 | require (bundle + '/working_copy.rb')
14 | require (support + '/lib/shelltokenize.rb')
15 | require (bundle + "/lib/Builder.rb")
16 |
17 | mup = Builder::XmlMarkup.new(:target => STDOUT)
18 |
19 | mup.html {
20 | mup.head {
21 | mup.title("CVS commit")
22 | mup.style( "@import 'file://"+bundle+"/Stylesheets/cvs_style.css';", "type" => "text/css")
23 | }
24 |
25 | mup.body {
26 | mup.h1("CVS Commit")
27 | STDOUT.flush
28 | mup.hr
29 |
30 | # Ignore files without changes
31 | #puts TextMate::selected_paths_for_shell
32 | working_copies = TextMate::selected_paths_array.map do |path|
33 | File.directory?(path) ?
34 | CVS::WorkingCopy.new(path) :
35 | CVS::VersionedFile.new(path)
36 | end
37 |
38 | #status_command = %Q{"#{cvs}" -nq update #{TextMate::selected_paths_for_shell}}
39 | #puts status_command
40 | #status_output = %x{#{status_command}}
41 | #puts status_output
42 | #paths = status_output.scan(/^(.)....(\s+)(.*)\n/)
43 | status = working_copies.inject({}) do |h,wc|
44 | case wc
45 | when CVS::WorkingCopy then h.update(wc.status)
46 | when CVS::VersionedFile then h.update(wc.path => wc.status)
47 | end
48 | h
49 | end
50 | paths = status.keys
51 |
52 |
53 | def paths_for_status(hash, *status)
54 | hash.inject([]) { |arr,(k,v)| arr << k if status.include?(v); arr }
55 | end
56 |
57 | # def status_to_paths()
58 | # paths = matches.collect { |m| m[2] }
59 | # paths.collect{|path| path.sub(/^#{CURRENT_DIR}/, "") }
60 | # end
61 |
62 | def matches_to_status(matches)
63 | matches.collect {|m| m[0]}
64 | end
65 |
66 | # Ignore files with '?', but report them to the user
67 | #unknown_paths = paths.select { |m| m[0] == '?' }
68 |
69 | unknown_paths = paths_for_status(status, :unknown)
70 | unknown_to_report_paths = unknown_paths.select { |path| ignore_file_pattern =~ path }
71 |
72 | #unknown_to_report_paths = paths.select{ |m| m[0] == '?' and not ignore_file_pattern =~ m[2]}
73 | unless unknown_to_report_paths.empty?
74 | mup.div( "class" => "info" ) {
75 | mup.text! "These files are not added to the repository, and so will not be committed:"
76 | mup.ul{ unknown_to_report_paths.each{ |path| mup.li(path) } }
77 | }
78 | end
79 |
80 | # Fail if we have conflicts -- cvs commit will fail, so let's
81 | # error out before the user gets too involved in the commit
82 | conflict_paths = paths_for_status(status, :conflicted)
83 |
84 | unless conflict_paths.empty?
85 | mup.div( "class" => "error" ) {
86 | mup.text! "Cannot continue; there are merge conflicts in files:"
87 | mup.ul{ conflict_paths.each { |path| mup.li(path) } }
88 | mup.text! "Canceled."
89 | }
90 | exit -1
91 | end
92 |
93 | # Remove the unknown paths from the commit
94 | commit_paths = paths.select { |path| [:modified, :added, :removed].include? status[path] }
95 |
96 | if commit_paths.empty?
97 | mup.div( "class" => "info" ) {
98 | mup.text! "File(s) not modified; nothing to commit."
99 | mup.ul{ unknown_paths.each { |path| mup.li(path) } }
100 | }
101 | exit 0
102 | end
103 |
104 | STDOUT.flush
105 |
106 | commit_status = commit_paths.map { |path| status[path].to_s[0,1].upcase }.join(":")
107 |
108 | commit_path_text = commit_paths.collect { |path| path.quote_filename_for_shell }.join(" ")
109 |
110 | commit_args = %x{"#{commit_tool}" --status #{commit_status} #{commit_path_text}}
111 |
112 | status = $CHILD_STATUS
113 | if status != 0
114 | mup.div( "class" => "error" ) {
115 | mup.text! "Canceled (#{status >> 8})."
116 | }
117 | exit -1
118 | end
119 |
120 | mup.div("class" => "command"){ mup.strong(%Q{#{cvs} commit }); mup.text!(commit_args) }
121 |
122 | mup.pre {
123 | STDOUT.flush
124 |
125 | puts working_copies.first.cvs(:commit, commit_args.gsub(working_copies.first.dirname, ''))
126 | }
127 | }
128 | }
--------------------------------------------------------------------------------
/Support/lib/Builder.rb:
--------------------------------------------------------------------------------
1 | # This is a single-file version of Jim Weirich's Builder suite version 1.2.3,
2 | # including some very minor tweaks required to make it work with Ruby 1.6.8.
3 | # Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
4 | # All rights reserved.
5 | #
6 | # Create XML markup easily. All (well, almost all) methods sent to
7 | # an XmlMarkup object will be translated to the equivalent XML
8 | # markup. Any method with a block will be treated as an XML markup
9 | # tag with nested markup in the block.
10 | #
11 | # Examples will demonstrate this easier than words. In the
12 | # following, +xm+ is an +XmlMarkup+ object.
13 | #
14 | # xm.em("emphasized") # => emphasized
15 | # xm.em { xmm.b("emp & bold") } # => emph & bold
16 | # xm.a("A Link", "href"=>"http://onestepback.org")
17 | # # => A Link
18 | # xm.div { br } # =>
19 | # xm.target("name"=>"compile", "option"=>"fast")
20 | # # =>
21 | # # NOTE: order of attributes is not specified.
22 | #
23 | # xm.instruct! #
24 | # xm.html { #
25 | # xm.head { #
26 | # xm.title("History") # History
27 | # } #
28 | # xm.body { #
29 | # xm.comment! "HI" #
30 | # xm.h1("Header") # Header
31 | # xm.p("paragraph") # paragraph
32 | # } #
33 | # } #
34 | #
35 |
36 |
37 | # blankslate.rb:
38 |
39 | #!/usr/bin/env ruby
40 | #--
41 | # Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
42 | # All rights reserved.
43 |
44 | # Permission is granted for use, copying, modification, distribution,
45 | # and distribution of modified versions of this work as long as the
46 | # above copyright notice is included.
47 | #++
48 |
49 | module Builder
50 |
51 | # BlankSlate provides an abstract base class with no predefined
52 | # methods (except for \_\_send__ and \_\_id__).
53 | # BlankSlate is useful as a base class when writing classes that
54 | # depend upon method_missing (e.g. dynamic proxies).
55 | class BlankSlate
56 | class << self
57 | def hide(name)
58 | undef_method name if
59 | instance_methods.include?(name.to_s) and
60 | name !~ /^(__|instance_eval)/
61 | end
62 | end
63 |
64 | instance_methods.each { |m| hide(m) }
65 | end
66 | end
67 |
68 | # Since Ruby is very dynamic, methods added to the ancestors of
69 | # BlankSlate after BlankSlate is defined will show up in the
70 | # list of available BlankSlate methods. We handle this by defining a hook in the Object and Kernel classes that will hide any defined
71 | module Kernel
72 | class << self
73 | alias_method :blank_slate_method_added, :method_added
74 | def method_added(name)
75 | blank_slate_method_added(name)
76 | return if self != Kernel
77 | Builder::BlankSlate.hide(name)
78 | end
79 | end
80 | end
81 |
82 | class Object
83 | class << self
84 | alias_method :blank_slate_method_added, :method_added
85 | def method_added(name)
86 | blank_slate_method_added(name)
87 | return if self != Object
88 | Builder::BlankSlate.hide(name)
89 | end
90 | end
91 | end
92 |
93 | # xmlbase.rb
94 | module Builder
95 |
96 | # Generic error for builder
97 | class IllegalBlockError < RuntimeError; end
98 |
99 | # XmlBase is a base class for building XML builders. See
100 | # Builder::XmlMarkup and Builder::XmlEvents for examples.
101 | class XmlBase < BlankSlate
102 |
103 | # Create an XML markup builder.
104 | #
105 | # out:: Object receiving the markup.1 +out+ must respond to
106 | # <<.
107 | # indent:: Number of spaces used for indentation (0 implies no
108 | # indentation and no line breaks).
109 | # initial:: Level of initial indentation.
110 | #
111 | def initialize(indent=0, initial=0)
112 | @indent = indent
113 | @level = initial
114 | @self = nil
115 | end
116 |
117 | # Create a tag named +sym+. Other than the first argument which
118 | # is the tag name, the arguements are the same as the tags
119 | # implemented via method_missing.
120 | def tag!(sym, *args, &block)
121 | self.__send__(sym, *args, &block)
122 | end
123 |
124 | # Create XML markup based on the name of the method. This method
125 | # is never invoked directly, but is called for each markup method
126 | # in the markup block.
127 | def method_missing(sym, *args, &block)
128 | text = nil
129 | attrs = nil
130 | sym = "#{sym}:#{args.shift}" if args.first.kind_of?(Symbol)
131 | args.each do |arg|
132 | case arg
133 | when Hash
134 | attrs ||= {}
135 | attrs.update(arg) # was merge!, which ruby 1.6.8 doesn't support
136 | else
137 | text ||= ''
138 | text << arg.to_s
139 | end
140 | end
141 | if block
142 | unless text.nil?
143 | raise ArgumentError, "XmlMarkup cannot mix a text argument with a block"
144 | end
145 | _capture_outer_self(block) if @self.nil?
146 | _indent
147 | _start_tag(sym, attrs)
148 | _newline
149 | _nested_structures(block)
150 | _indent
151 | _end_tag(sym)
152 | _newline
153 | elsif text.nil?
154 | _indent
155 | _start_tag(sym, attrs, true)
156 | _newline
157 | else
158 | _indent
159 | _start_tag(sym, attrs)
160 | text! text
161 | _end_tag(sym)
162 | _newline
163 | end
164 | @target
165 | end
166 |
167 | # Append text to the output target. Escape any markup. May be
168 | # used within the markup brakets as:
169 | #
170 | # builder.p { |b| b.br; b.text! "HI" } #=>
HI
171 | def text!(text)
172 | _text(_escape(text))
173 | end
174 |
175 | # Append text to the output target without escaping any markup.
176 | # May be used within the markup brakets as:
177 | #
178 | # builder.p { |x| x << "
HI" } #=>
HI
179 | #
180 | # This is useful when using non-builder enabled software that
181 | # generates strings. Just insert the string directly into the
182 | # builder without changing the inserted markup.
183 | #
184 | # It is also useful for stacking builder objects. Builders only
185 | # use << to append to the target, so by supporting this
186 | # method/operation builders can use other builders as their
187 | # targets.
188 | def <<(text)
189 | _text(text)
190 | end
191 |
192 | # For some reason, nil? is sent to the XmlMarkup object. If nil?
193 | # is not defined and method_missing is invoked, some strange kind
194 | # of recursion happens. Since nil? won't ever be an XML tag, it
195 | # is pretty safe to define it here. (Note: this is an example of
196 | # cargo cult programming,
197 | # cf. http://fishbowl.pastiche.org/2004/10/13/cargo_cult_programming).
198 | def nil?
199 | false
200 | end
201 |
202 | private
203 |
204 | def _escape(text)
205 | text.
206 | gsub(%r{&}, '&').
207 | gsub(%r{<}, '<').
208 | gsub(%r{>}, '>')
209 | end
210 |
211 | def _capture_outer_self(block)
212 | @self = eval("self", block)
213 | end
214 |
215 | def _newline
216 | return if @indent == 0
217 | text! "\n"
218 | end
219 |
220 | def _indent
221 | return if @indent == 0 || @level == 0
222 | text!(" " * (@level * @indent))
223 | end
224 |
225 | def _nested_structures(block)
226 | @level += 1
227 | block.call(self)
228 | ensure
229 | @level -= 1
230 | end
231 | end
232 | end
233 |
234 | # xmlmarkup.rb
235 | module Builder
236 |
237 | # Create XML markup easily. All (well, almost all) methods sent to
238 | # an XmlMarkup object will be translated to the equivalent XML
239 | # markup. Any method with a block will be treated as an XML markup
240 | # tag with nested markup in the block.
241 | #
242 | # Examples will demonstrate this easier than words. In the
243 | # following, +xm+ is an +XmlMarkup+ object.
244 | #
245 | # xm.em("emphasized") # => emphasized
246 | # xm.em { xmm.b("emp & bold") } # => emph & bold
247 | # xm.a("A Link", "href"=>"http://onestepback.org")
248 | # # => A Link
249 | # xm.div { br } # =>
250 | # xm.target("name"=>"compile", "option"=>"fast")
251 | # # =>
252 | # # NOTE: order of attributes is not specified.
253 | #
254 | # xm.instruct! #
255 | # xm.html { #
256 | # xm.head { #
257 | # xm.title("History") # History
258 | # } #
259 | # xm.body { #
260 | # xm.comment! "HI" #
261 | # xm.h1("Header") # Header
262 | # xm.p("paragraph") # paragraph
263 | # } #
264 | # } #
265 | #
266 | # == Notes:
267 | #
268 | # * The order that attributes are inserted in markup tags is
269 | # undefined.
270 | #
271 | # * Sometimes you wish to insert text without enclosing tags. Use
272 | # the text! method to accomplish this.
273 | #
274 | # Example:
275 | #
276 | # xm.div { #
277 | # xm.text! "line"; xm.br # line
278 | # xm.text! "another line"; xmbr # another line
279 | # } #
280 | #
281 | # * The special XML characters <, >, and & are converted to <,
282 | # > and & automatically. Use the << operation to
283 | # insert text without modification.
284 | #
285 | # * Sometimes tags use special characters not allowed in ruby
286 | # identifiers. Use the tag! method to handle these
287 | # cases.
288 | #
289 | # Example:
290 | #
291 | # xml.tag!("SOAP:Envelope") { ... }
292 | #
293 | # will produce ...
294 | #
295 | # ... "
296 | #
297 | # tag! will also take text and attribute arguments (after
298 | # the tag name) like normal markup methods. (But see the next
299 | # bullet item for a better way to handle XML namespaces).
300 | #
301 | # * Direct support for XML namespaces is now available. If the
302 | # first argument to a tag call is a symbol, it will be joined to
303 | # the tag to produce a namespace:tag combination. It is easier to
304 | # show this than describe it.
305 | #
306 | # xml.SOAP :Envelope do ... end
307 | #
308 | # Just put a space before the colon in a namespace to produce the
309 | # right form for builder (e.g. "SOAP:Envelope" =>
310 | # "xml.SOAP :Envelope")
311 | #
312 | # * XmlMarkup builds the markup in any object (called a _target_)
313 | # that accepts the << method. If no target is given,
314 | # then XmlMarkup defaults to a string target.
315 | #
316 | # Examples:
317 | #
318 | # xm = Builder::XmlMarkup.new
319 | # result = xm.title("yada")
320 | # # result is a string containing the markup.
321 | #
322 | # buffer = ""
323 | # xm = Builder::XmlMarkup.new(buffer)
324 | # # The markup is appended to buffer (using <<)
325 | #
326 | # xm = Builder::XmlMarkup.new(STDOUT)
327 | # # The markup is written to STDOUT (using <<)
328 | #
329 | # xm = Builder::XmlMarkup.new
330 | # x2 = Builder::XmlMarkup.new(:target=>xm)
331 | # # Markup written to +x2+ will be send to +xm+.
332 | #
333 | # * Indentation is enabled by providing the number of spaces to
334 | # indent for each level as a second argument to XmlBuilder.new.
335 | # Initial indentation may be specified using a third parameter.
336 | #
337 | # Example:
338 | #
339 | # xm = Builder.new(:ident=>2)
340 | # # xm will produce nicely formatted and indented XML.
341 | #
342 | # xm = Builder.new(:indent=>2, :margin=>4)
343 | # # xm will produce nicely formatted and indented XML with 2
344 | # # spaces per indent and an over all indentation level of 4.
345 | #
346 | # builder = Builder::XmlMarkup.new(:target=>$stdout, :indent=>2)
347 | # builder.name { |b| b.first("Jim"); b.last("Weirich) }
348 | # # prints:
349 | # #
350 | # # Jim
351 | # # Weirich
352 | # #
353 | #
354 | # * The instance_eval implementation which forces self to refer to
355 | # the message receiver as self is now obsolete. We now use normal
356 | # block calls to execute the markup block. This means that all
357 | # markup methods must now be explicitly send to the xml builder.
358 | # For instance, instead of
359 | #
360 | # xml.div { strong("text") }
361 | #
362 | # you need to write:
363 | #
364 | # xml.div { xml.strong("text") }
365 | #
366 | # Although more verbose, the subtle change in semantics within the
367 | # block was found to be prone to error. To make this change a
368 | # little less cumbersome, the markup block now gets the markup
369 | # object sent as an argument, allowing you to use a shorter alias
370 | # within the block.
371 | #
372 | # For example:
373 | #
374 | # xml_builder = Builder::XmlMarkup.new
375 | # xml_builder.div { |xml|
376 | # xml.stong("text")
377 | # }
378 | #
379 | class XmlMarkup < XmlBase
380 |
381 | # Create an XML markup builder. Parameters are specified by an
382 | # option hash.
383 | #
384 | # :target=>target_object::
385 | # Object receiving the markup. +out+ must respond to the
386 | # << operator. The default is a plain string target.
387 | # :indent=>indentation::
388 | # Number of spaces used for indentation. The default is no
389 | # indentation and no line breaks.
390 | # :margin=>initial_indentation_level::
391 | # Amount of initial indentation (specified in levels, not
392 | # spaces).
393 | #
394 | def initialize(options={})
395 | indent = options[:indent] || 0
396 | margin = options[:margin] || 0
397 | super(indent, margin)
398 | @target = options[:target] || ""
399 | end
400 |
401 | # Return the target of the builder.
402 | def target!
403 | @target
404 | end
405 |
406 | def comment!(comment_text)
407 | _ensure_no_block block_given?
408 | _special("", comment_text, nil)
409 | end
410 |
411 | # Insert an XML declaration into the XML markup.
412 | #
413 | # For example:
414 | #
415 | # xml.declare! :ELEMENT, :blah, "yada"
416 | # # =>
417 | def declare!(inst, *args, &block)
418 | _indent
419 | @target << ""
435 | _newline
436 | end
437 |
438 | # Insert a processing instruction into the XML markup. E.g.
439 | #
440 | # For example:
441 | #
442 | # xml.instruct!
443 | # #=>
444 | # xml.instruct! :aaa, :bbb=>"ccc"
445 | # #=>
446 | #
447 | def instruct!(directive_tag=:xml, attrs={})
448 | _ensure_no_block block_given?
449 | if directive_tag == :xml
450 | a = { :version=>"1.0", :encoding=>"UTF-8" }
451 | attrs = a.dup.update attrs # was merge, which isn't available with ruby 1.6.8
452 | end
453 | _special(
454 | "#{directive_tag}",
455 | "?>",
456 | nil,
457 | attrs,
458 | [:version, :encoding, :standalone])
459 | end
460 |
461 | private
462 |
463 | # NOTE: All private methods of a builder object are prefixed when
464 | # a "_" character to avoid possible conflict with XML tag names.
465 |
466 | # Insert text directly in to the builder's target.
467 | def _text(text)
468 | @target << text
469 | end
470 |
471 | # Insert special instruction.
472 | def _special(open, close, data=nil, attrs=nil, order=[])
473 | _indent
474 | @target << open
475 | @target << data if data
476 | _insert_attributes(attrs, order) if attrs
477 | @target << close
478 | _newline
479 | end
480 |
481 | # Start an XML tag. If end_too is true, then the start
482 | # tag is also the end tag (e.g.
483 | def _start_tag(sym, attrs, end_too=false)
484 | @target << "<#{sym}"
485 | _insert_attributes(attrs)
486 | @target << "/" if end_too
487 | @target << ">"
488 | end
489 |
490 | # Insert an ending tag.
491 | def _end_tag(sym)
492 | @target << "#{sym}>"
493 | end
494 |
495 | # Insert the attributes (given in the hash).
496 | def _insert_attributes(attrs, order=[])
497 | return if attrs.nil?
498 | order.each do |k|
499 | v = attrs[k]
500 | @target << %{ #{k}="#{v}"} if v
501 | end
502 | attrs.each do |k, v|
503 | @target << %{ #{k}="#{v}"} unless order.member?(k)
504 | end
505 | end
506 |
507 | def _ensure_no_block(got_block)
508 | if got_block
509 | fail IllegalBlockError,
510 | "Blocks are not allowed on XML instructions"
511 | end
512 | end
513 |
514 | end
515 |
516 | end
517 |
518 |
--------------------------------------------------------------------------------