├── Templates
├── Weblog Post (HTML)
│ ├── untitled.blog.html
│ └── info.plist
├── Weblog Post (Text)
│ ├── untitled.blog.txt
│ └── info.plist
├── Weblog Post
│ ├── untitled.blog.markdown
│ └── info.plist
└── Weblog Post (Textile)
│ ├── untitled.blog.textile
│ └── info.plist
├── LICENSE
├── Snippets
├── Tags (tags).plist
├── Pings (pings).plist
├── Keywords (keyw).plist
├── Comments (comments).plist
├── Date (date).plist
├── Title (title).plist
├── Ping (ping).plist
└── Cut (cut).tmSnippet
├── Commands
├── Fetch Post.plist
├── View Post.tmCommand
├── Blog (blog).tmCommand
├── Post to Weblog.plist
├── Help.tmCommand
├── Preview.tmCommand
├── Setup Blogs.tmCommand
└── Fetch Categories.tmCommand
├── Support
├── lib
│ ├── keychain.rb
│ ├── metaweblog.rb
│ ├── browser.rb
│ ├── blogging.rb
│ └── redcloth.rb
└── help.markdown
├── Preferences
├── Folding - Markdown.tmPreferences
└── Folding - HTML.tmPreferences
├── DragCommands
└── Upload Image.tmDragCommand
├── README.mdown
├── Syntaxes
├── Blog (Text).plist
├── Blog (HTML).plist
├── Blog (Textile).plist
└── Blog (Markdown).plist
└── info.plist
/Templates/Weblog Post (HTML)/untitled.blog.html:
--------------------------------------------------------------------------------
1 | Title: ${TM_BLOG_TITLE}
2 | ${TM_BLOG_HEADER}
3 | Main entry text
4 |
5 | ✂------✂------✂------✂------✂------✂------✂------✂------✂------✂------
6 |
7 | Main entry continued
8 |
--------------------------------------------------------------------------------
/Templates/Weblog Post (Text)/untitled.blog.txt:
--------------------------------------------------------------------------------
1 | Title: ${TM_BLOG_TITLE}
2 | ${TM_BLOG_HEADER}
3 | Main entry text
4 |
5 | ✂------✂------✂------✂------✂------✂------✂------✂------✂------✂------
6 |
7 | Main entry continued
8 |
--------------------------------------------------------------------------------
/Templates/Weblog Post/untitled.blog.markdown:
--------------------------------------------------------------------------------
1 | Title: ${TM_BLOG_TITLE}
2 | ${TM_BLOG_HEADER}
3 | Main entry text
4 |
5 | ✂------✂------✂------✂------✂------✂------✂------✂------✂------✂------
6 |
7 | Main entry continued
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Permission to copy, use, modify, sell and distribute this
2 | software is granted. This software is provided "as is" without
3 | express or implied warranty, and with no claim as to its
4 | suitability for any purpose.
5 |
--------------------------------------------------------------------------------
/Templates/Weblog Post (Textile)/untitled.blog.textile:
--------------------------------------------------------------------------------
1 | Title: ${TM_BLOG_TITLE}
2 | ${TM_BLOG_HEADER}
3 | Main entry text
4 |
5 | ✂------✂------✂------✂------✂------✂------✂------✂------✂------✂------
6 |
7 | Main entry continued
8 |
--------------------------------------------------------------------------------
/Snippets/Tags (tags).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | content
6 | Tags:
7 | name
8 | Tags
9 | scope
10 | text.blog
11 | tabTrigger
12 | tags
13 | uuid
14 | D947660B-DD37-44C3-B4A5-7003B90CD0DD
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Snippets/Pings (pings).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | content
6 | Pings: On
7 | name
8 | Pings
9 | scope
10 | text.blog
11 | tabTrigger
12 | pings
13 | uuid
14 | 2238B6A6-0961-4DC6-917D-477F776CAF9D
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Snippets/Keywords (keyw).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | content
6 | Keywords:
7 | name
8 | Keywords
9 | scope
10 | text.blog
11 | tabTrigger
12 | keyw
13 | uuid
14 | 5CCBC664-2477-4C8C-8B8B-7B57DB15DB23
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Snippets/Comments (comments).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | content
6 | Comments: On
7 | name
8 | Comments
9 | scope
10 | text.blog
11 | tabTrigger
12 | comments
13 | uuid
14 | 6CEE1440-987D-4E37-BA50-9AD23784D2E5
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Snippets/Date (date).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | content
6 | Date: `date +"%Y-%m-%d %H:%M:%S"`
7 | name
8 | Date
9 | scope
10 | text.blog
11 | tabTrigger
12 | date
13 | uuid
14 | B55BA9A1-0F22-45F4-B7C8-B216BF038AF9
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Snippets/Title (title).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | content
6 | Title:
7 | name
8 | Title
9 | scope
10 | text.blog text.html, text.blog text.plain
11 | tabTrigger
12 | title
13 | uuid
14 | 5C0A6073-6592-4841-83A0-C03D742074E5
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Snippets/Ping (ping).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | content
6 | Ping: ${1:http://www.example.com/post/ping/}
7 |
8 | name
9 | Ping
10 | scope
11 | text.blog
12 | tabTrigger
13 | ping
14 | uuid
15 | 853BB979-1DA2-4D78-B50D-092B80A1877F
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Snippets/Cut (cut).tmSnippet:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | content
6 | ✂------✂------✂------✂------✂------✂------✂------✂------✂------✂------
7 |
8 | name
9 | Cut
10 | scope
11 | text.blog
12 | tabTrigger
13 | cut
14 | uuid
15 | 6F1CF327-E4C8-49D8-BD30-FD6F987AE23A
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Commands/Fetch Post.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18 -rjcode -Ku
9 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/blogging.rb"
10 | Blogging.new.fetch
11 | input
12 | none
13 | name
14 | Fetch Post
15 | output
16 | openAsNewDocument
17 | uuid
18 | FA5DC73E-AAE0-4C69-86E1-87B9E0390FD0
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Support/lib/keychain.rb:
--------------------------------------------------------------------------------
1 | # Simple interface for reading and writing Internet passwords to
2 | # the KeyChain.
3 | require "shellwords"
4 |
5 | module KeyChain
6 | class << self
7 | def add_internet_password(user, proto, host, path, pass)
8 | %x{security add-internet-password -a #{user.shellescape} -s #{host.shellescape} -r #{proto.shellescape} -p #{path.shellescape} -w #{pass.shellescape}}
9 | end
10 | def find_internet_password(user, proto, host, path)
11 | result = %x{security find-internet-password -g -a #{user.shellescape} -s #{host.shellescape} -p #{path.shellescape} -r #{proto.shellescape} 2>&1 >/dev/null}
12 | result =~ /^password: "(.*)"$/ ? $1 : nil
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/Commands/View Post.tmCommand:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18 -rjcode -Ku
9 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/blogging.rb"
10 | Blogging.new.view
11 | input
12 | document
13 | name
14 | View Online Version
15 | output
16 | discard
17 | scope
18 | text.blog
19 | uuid
20 | BD6E6210-F4A3-4821-BA43-A9DFF4178704
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Commands/Blog (blog).tmCommand:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18 -rjcode -Ku
9 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/blogging.rb"
10 | Blogging.new.choose_blog_endpoint
11 | input
12 | none
13 | name
14 | Blog
15 | output
16 | afterSelectedText
17 | scope
18 | text.blog
19 | tabTrigger
20 | blog
21 | uuid
22 | C316CEDC-A4E1-41DE-B102-CDF78845ACF3
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Commands/Post to Weblog.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18 -rjcode -Ku
9 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/blogging.rb"
10 | Blogging.new.post_or_update
11 | input
12 | document
13 | keyEquivalent
14 | ^@p
15 | name
16 | Post to Blog
17 | output
18 | showAsTooltip
19 | scope
20 | text.blog
21 | uuid
22 | 60853977-B0D2-4776-A3D9-4B6C09E18596
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Templates/Weblog Post (HTML)/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | command
6 | if [[ ! -f "$TM_NEW_FILE" ]]; then
7 | TM_BLOG_TITLE=${TM_NEW_FILE_BASENAME%%.*} \
8 | TM_BLOG_HEADER=${TM_BLOG_ENDPOINT:+Blog: $TM_BLOG_ENDPOINT$'\n'} \
9 | TM_YEAR=`date +%Y` \
10 | TM_DATE=`date +%Y-%m-%d` \
11 | TM_ISO_DATE=`date +"%Y-%m-%d %H:%M:%S"` \
12 | perl -pe 's/\$\{([^}]*)\}/$ENV{$1}/g' < untitled.blog.html > "$TM_NEW_FILE"
13 | fi
14 | extension
15 | blog.html
16 | name
17 | Blog Post (HTML)
18 | uuid
19 | E7B58845-506D-4065-9835-0D37DCFC02D2
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Templates/Weblog Post (Text)/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | command
6 | if [[ ! -f "$TM_NEW_FILE" ]]; then
7 | TM_BLOG_TITLE=${TM_NEW_FILE_BASENAME%%.*} \
8 | TM_BLOG_HEADER=${TM_BLOG_ENDPOINT:+Blog: $TM_BLOG_ENDPOINT$'\n'} \
9 | TM_YEAR=`date +%Y` \
10 | TM_DATE=`date +%Y-%m-%d` \
11 | TM_ISO_DATE=`date +"%Y-%m-%d %H:%M:%S"` \
12 | perl -pe 's/\$\{([^}]*)\}/$ENV{$1}/g' < untitled.blog.txt > "$TM_NEW_FILE"
13 | fi
14 | extension
15 | blog.txt
16 | name
17 | Blog Post (Text)
18 | uuid
19 | 96F84D65-3CD5-4088-9360-15131E1611DF
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Templates/Weblog Post/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | command
6 | if [[ ! -f "$TM_NEW_FILE" ]]; then
7 | TM_BLOG_TITLE=${TM_NEW_FILE_BASENAME%%.*} \
8 | TM_BLOG_HEADER=${TM_BLOG_ENDPOINT:+Blog: $TM_BLOG_ENDPOINT$'\n'} \
9 | TM_YEAR=`date +%Y` \
10 | TM_DATE=`date +%Y-%m-%d` \
11 | TM_ISO_DATE=`date +"%Y-%m-%d %H:%M:%S"` \
12 | perl -pe 's/\$\{([^}]*)\}/$ENV{$1}/g' < untitled.blog.markdown > "$TM_NEW_FILE"
13 | fi
14 | extension
15 | blog.markdown
16 | name
17 | Blog Post (Markdown)
18 | uuid
19 | F0B36FAE-07A0-46C9-992F-5901327FE266
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Templates/Weblog Post (Textile)/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | command
6 | if [[ ! -f "$TM_NEW_FILE" ]]; then
7 | TM_BLOG_TITLE=${TM_NEW_FILE_BASENAME%%.*} \
8 | TM_BLOG_HEADER=${TM_BLOG_ENDPOINT:+Blog: $TM_BLOG_ENDPOINT$'\n'} \
9 | TM_YEAR=`date +%Y` \
10 | TM_DATE=`date +%Y-%m-%d` \
11 | TM_ISO_DATE=`date +"%Y-%m-%d %H:%M:%S"` \
12 | perl -pe 's/\$\{([^}]*)\}/$ENV{$1}/g' < untitled.blog.textile > "$TM_NEW_FILE"
13 | fi
14 | extension
15 | blog.textile
16 | name
17 | Blog Post (Textile)
18 | uuid
19 | 60F07B75-41FD-473F-A390-9E821D880469
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Commands/Help.tmCommand:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | bundleUUID
8 | 79741B2E-271D-4CBC-A61A-380C83D36863
9 | command
10 | . "$TM_SUPPORT_PATH/lib/webpreview.sh"
11 | html_header "Blogging Bundle Help" "Blogging"
12 | "$TM_SUPPORT_PATH/lib/markdown_to_help.rb" "$TM_BUNDLE_SUPPORT/help.markdown"
13 | html_footer
14 |
15 | input
16 | selection
17 | name
18 | Help
19 | output
20 | showAsHTML
21 | scope
22 | text.blog
23 | uuid
24 | 17B2F39B-5CCB-4B0E-B305-8C27BED56887
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Commands/Preview.tmCommand:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18 -rjcode -Ku
9 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/blogging.rb"
10 | Blogging.new.preview
11 | input
12 | document
13 | keyEquivalent
14 | ^~@p
15 | name
16 | Preview
17 | output
18 | showAsHTML
19 | scope
20 | text.blog, text.blog text.html.markdown, text.blog text.plain, text.blog text.html.textile, text.blog text.html
21 | uuid
22 | 10CFDE4C-E433-48F8-AB94-17E1FBEFC074
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Commands/Setup Blogs.tmCommand:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/bin/bash
9 | AccountFile="$HOME/Library/Preferences/com.macromates.textmate.blogging.txt"
10 | if [[ ! -e "$AccountFile" ]]; then
11 | echo "# List of Blogs
12 | #
13 | # Enter a blog name followed by the endpoint URL
14 | #
15 | # Blog Name URL
16 | example http://user@example.com/xmlrpc" > "$AccountFile"
17 | fi
18 | "$TM_SUPPORT_PATH/bin/mate" "$AccountFile"
19 |
20 | input
21 | none
22 | name
23 | Setup Blogs
24 | output
25 | discard
26 | uuid
27 | 8DCBE1EB-A3CC-4559-872E-34A3643F0BC4
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Preferences/Folding - Markdown.tmPreferences:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | name
6 | Folding (Markdown)
7 | scope
8 | text.blog.markdown
9 | settings
10 |
11 | foldingStartMarker
12 | (?x)
13 | (<(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|form|dl)\b.*?>
14 | |<!--(?!.*-->)
15 | |\{\s*($|\?>\s*$|//|/\*(.*\*/\s*$|(?!.*?\*/)))
16 | )
17 | foldingStopMarker
18 | (?x)
19 | (</(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|form|dl)>
20 | |^\s*-->
21 | |(^|\s)\}
22 | )
23 |
24 | uuid
25 | 3E820DE3-BAAC-4B77-BE9A-1663DFBFFC5E
26 |
27 |
28 |
--------------------------------------------------------------------------------
/DragCommands/Upload Image.tmDragCommand:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | command
8 | #!/usr/bin/env ruby18
9 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/blogging.rb"
10 | Blogging.new.upload_image
11 | draggedFileExtensions
12 |
13 | png
14 | jpg
15 | jpeg
16 | gif
17 |
18 | input
19 | selection
20 | name
21 | Upload Image
22 | output
23 | insertAsSnippet
24 | scope
25 | text.blog text.html.markdown, text.blog text.html.textile, text.blog text.html, text.blog text.plain
26 | uuid
27 | 7A2373C1-A300-49B5-9F83-676612D8D5B5
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Preferences/Folding - HTML.tmPreferences:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | name
6 | Folding (HTML)
7 | scope
8 | text.blog.html
9 | settings
10 |
11 | foldingStartMarker
12 | (?x)
13 | (<(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|form|dl)\b.*?>
14 | |<!--(?!.*--\s*>)
15 | |\{\{?(if|foreach|capture|literal|foreach|php|section|strip)
16 | |\{\s*($|\?>\s*$|//|/\*(.*\*/\s*$|(?!.*?\*/)))
17 | )
18 | foldingStopMarker
19 | (?x)
20 | (</(?i:head|body|table|thead|tbody|tfoot|tr|div|select|fieldset|style|script|ul|ol|form|dl)>
21 | |^(?!.*?<!--).*?--\s*>
22 | |\{\{?/(if|foreach|capture|literal|foreach|php|section|strip)
23 | |^[^{]*\}
24 | )
25 |
26 | uuid
27 | EF0D28C3-6199-4379-B5C7-7E9C678C4005
28 |
29 |
30 |
--------------------------------------------------------------------------------
/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”.
--------------------------------------------------------------------------------
/Support/lib/metaweblog.rb:
--------------------------------------------------------------------------------
1 | require 'xmlrpc/client'
2 | require "net/https" # OpenSSL::SSL::VERIFY_NONE
3 |
4 | class MetaWeblogClient < XMLRPC::Client
5 | def initialize(*args)
6 | super(*args)
7 | @http.verify_mode = OpenSSL::SSL::VERIFY_NONE # squelch Net::HTTP warning
8 | self.http_header_extra = { "User-Agent" => "TextMate/Blogging Bundle (Mac OS X; http://macromates.com/blog/archives/2006/06/19/blogging-from-textmate/)" }
9 | end
10 | def get_post(post_id, username, password)
11 | call("metaWeblog.getPost", "#{post_id}", "#{username}", "#{password}")
12 | end
13 | def edit_post(post_id, username, password, post, publish)
14 | call("metaWeblog.editPost", "#{post_id}", "#{username}", "#{password}", post, publish)
15 | end
16 | def new_post(blog_id, username, password, post, publish)
17 | call("metaWeblog.newPost", "#{blog_id}", "#{username}", "#{password}", post, publish)
18 | end
19 | def get_recent_posts(blog_id, username, password, number)
20 | call("metaWeblog.getRecentPosts", "#{blog_id}", "#{username}", "#{password}", number)
21 | end
22 | def new_media_object(blog_id, username, password, data)
23 | call("metaWeblog.newMediaObject", "#{blog_id}", "#{username}", "#{password}", data)
24 | end
25 |
26 | # These methods are properCased to match the XMLRPC method names.
27 | def getPost(post_id, username, password)
28 | call("metaWeblog.getPost", "#{post_id}", "#{username}", "#{password}")
29 | end
30 | def editPost(post_id, username, password, post, publish)
31 | call("metaWeblog.editPost", "#{post_id}", "#{username}", "#{password}", post, publish)
32 | end
33 | def newPost(blog_id, username, password, post, publish)
34 | call("metaWeblog.newPost", "#{blog_id}", "#{username}", "#{password}", post, publish)
35 | end
36 | def getRecentPosts(blog_id, username, password, number)
37 | call("metaWeblog.getRecentPosts", "#{blog_id}", "#{username}", "#{password}", number)
38 | end
39 | def newMediaObject(blog_id, username, password, data)
40 | call("metaWeblog.newMediaObject", "#{blog_id}", "#{username}", "#{password}", data)
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/Syntaxes/Blog (Text).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | fileTypes
6 |
7 | blog.txt
8 |
9 | firstLineMatch
10 | ^Type: Blog Post \(Text\)
11 | keyEquivalent
12 | ^~B
13 | name
14 | Blog — Text
15 | patterns
16 |
17 |
18 | captures
19 |
20 | 1
21 |
22 | name
23 | keyword.other.blog
24 |
25 | 2
26 |
27 | name
28 | punctuation.separator.key-value.blog
29 |
30 | 3
31 |
32 | name
33 | string.unquoted.blog
34 |
35 |
36 | match
37 | ^([Tt]itle|[Dd]ate|[Bb]asename|[Ss]lug|[Kk]eywords|[Bb]log|[Tt]ype|[Ll]ink|[Pp]ost|[Tt]ags|[Cc]omments|[Pp]ings?|[Cc]ategory|[Ss]tatus|[Ff]ormat|[Pp]ostformat)(:)\s*(.*)$\n?
38 | name
39 | meta.header.blog
40 |
41 |
42 | match
43 | ^([A-Za-z0-9]+):\s*(.*)$\n?
44 | name
45 | invalid.illegal.meta.header.blog
46 |
47 |
48 | begin
49 | ^(?![A-Za-z0-9]+:)
50 | end
51 | ^(?=not)possible$
52 | name
53 | text.plain
54 | patterns
55 |
56 |
57 | match
58 | ^✂-[✂-]+$\n
59 | name
60 | meta.separator.blog
61 |
62 |
63 | include
64 | text.plain
65 |
66 |
67 |
68 |
69 | scopeName
70 | text.blog
71 | uuid
72 | B2CCDFF8-0FB3-492A-8761-D31FF0FAC448
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Syntaxes/Blog (HTML).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | fileTypes
6 |
7 | blog.html
8 |
9 | firstLineMatch
10 | ^Type: Blog Post \(HTML\)
11 | keyEquivalent
12 | ^~B
13 | name
14 | Blog — HTML
15 | patterns
16 |
17 |
18 | captures
19 |
20 | 1
21 |
22 | name
23 | keyword.other.blog
24 |
25 | 2
26 |
27 | name
28 | punctuation.separator.key-value.blog
29 |
30 | 3
31 |
32 | name
33 | string.unquoted.blog
34 |
35 |
36 | match
37 | ^([Tt]itle|[Dd]ate|[Bb]asename|[Ss]lug|[Kk]eywords|[Bb]log|[Tt]ype|[Ll]ink|[Pp]ost|[Tt]ags|[Cc]omments|[Pp]ings?|[Cc]ategory|[Ss]tatus|[Ff]ormat|[Pp]ostformat)(:)\s*(.*)$\n?
38 | name
39 | meta.header.blog
40 |
41 |
42 | match
43 | ^([A-Za-z0-9]+):\s*(.*)$\n?
44 | name
45 | invalid.illegal.meta.header.blog
46 |
47 |
48 | begin
49 | ^(?![A-Za-z0-9]+:)
50 | end
51 | ^(?=not)possible$
52 | name
53 | text.html
54 | patterns
55 |
56 |
57 | match
58 | ^✂-[✂-]+$\n
59 | name
60 | meta.separator.blog
61 |
62 |
63 | include
64 | text.html.basic
65 |
66 |
67 |
68 |
69 | scopeName
70 | text.blog.html
71 | uuid
72 | E46F5C50-5D16-4B5C-8FBB-686BD3768284
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Syntaxes/Blog (Textile).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | fileTypes
6 |
7 | blog.textile
8 |
9 | firstLineMatch
10 | ^Type: Blog Post \(Textile\)
11 | keyEquivalent
12 | ^~B
13 | name
14 | Blog — Textile
15 | patterns
16 |
17 |
18 | captures
19 |
20 | 1
21 |
22 | name
23 | keyword.other.blog
24 |
25 | 2
26 |
27 | name
28 | punctuation.separator.key-value.blog
29 |
30 | 3
31 |
32 | name
33 | string.unquoted.blog
34 |
35 |
36 | match
37 | ^([Tt]itle|[Dd]ate|[Bb]asename|[Ss]lug|[Kk]eywords|[Bb]log|[Tt]ype|[Ll]ink|[Pp]ost|[Tt]ags|[Cc]omments|[Pp]ings?|[Cc]ategory|[Ss]tatus|[Ff]ormat|[Pp]ostformat)(:)\s*(.*)$\n?
38 | name
39 | meta.header.blog
40 |
41 |
42 | match
43 | ^([A-Za-z0-9]+):\s*(.*)$\n?
44 | name
45 | invalid.illegal.meta.header.blog
46 |
47 |
48 | begin
49 | ^(?![A-Za-z0-9]+:)
50 | end
51 | ^(?=not)possible$
52 | name
53 | text.html.textile
54 | patterns
55 |
56 |
57 | match
58 | ^✂-[✂-]+$\n
59 | name
60 | meta.separator.blog
61 |
62 |
63 | include
64 | text.html.textile
65 |
66 |
67 |
68 |
69 | scopeName
70 | text.blog.textile
71 | uuid
72 | 32E65853-CDBD-401A-ADBE-F94F195249BE
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Syntaxes/Blog (Markdown).plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | fileTypes
6 |
7 | blog.md
8 | blog.markdown
9 | blog.mdown
10 | blog.mkdn
11 |
12 | firstLineMatch
13 | ^Type: Blog Post \(Markdown\)
14 | keyEquivalent
15 | ^~B
16 | name
17 | Blog — Markdown
18 | patterns
19 |
20 |
21 | captures
22 |
23 | 1
24 |
25 | name
26 | keyword.other.blog
27 |
28 | 2
29 |
30 | name
31 | punctuation.separator.key-value.blog
32 |
33 | 3
34 |
35 | name
36 | string.unquoted.blog
37 |
38 |
39 | match
40 | ^([Tt]itle|[Dd]ate|[Bb]asename|[Ss]lug|[Kk]eywords|[Bb]log|[Tt]ype|[Ll]ink|[Pp]ost|[Tt]ags|[Cc]omments|[Pp]ings?|[Cc]ategory|[Ss]tatus|[Ff]ormat|[Pp]ostformat)(:)\s*(.*)$\n?
41 | name
42 | meta.header.blog
43 |
44 |
45 | match
46 | ^([A-Za-z0-9]+):\s*(.*)$\n?
47 | name
48 | invalid.illegal.meta.header.blog
49 |
50 |
51 | begin
52 | ^(?![A-Za-z0-9]+:)
53 | end
54 | ^(?=not)possible$
55 | name
56 | text.html.markdown
57 | patterns
58 |
59 |
60 | match
61 | ^✂-[✂-]+$\n
62 | name
63 | meta.separator.blog
64 |
65 |
66 | include
67 | text.html.markdown
68 |
69 |
70 |
71 |
72 | scopeName
73 | text.blog.markdown
74 | uuid
75 | 6AA68B5B-18B8-4922-9CED-0E2295582955
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Commands/Fetch Categories.tmCommand:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | beforeRunningCommand
6 | nop
7 | bundleUUID
8 | 79741B2E-271D-4CBC-A61A-380C83D36863
9 | command
10 | #!/usr/bin/env ruby18 -rjcode -Ku
11 | require "#{ENV['TM_SUPPORT_PATH']}/lib/progress"
12 | require "#{ENV['TM_SUPPORT_PATH']}/lib/exit_codes"
13 | require "#{ENV['TM_SUPPORT_PATH']}/lib/escape"
14 | require "#{ENV['TM_SUPPORT_PATH']}/lib/ui"
15 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/blogging"
16 | require 'xmlrpc/client'
17 |
18 | # fetches available categories (only tested with Wordpress)
19 |
20 | def select_from_cats(cats)
21 | # the metaWeblog API says the result is a per-category struct containing a
22 | # description, but nothing about a categoryName (which e.g. WP will *also*
23 | # give us). So we prefer a categoryName, then falls back on description,
24 | # though Typo just returns strings instead of structs, so we handle that as
25 | # well
26 | names = cats.map do |p|
27 | if Hash === p && p.has_key?('categoryName')
28 | p['categoryName']
29 | elsif Hash === p && p.has_key?('description')
30 | p['description']
31 | else
32 | p.to_s
33 | end
34 | end
35 |
36 | names.sort! { |a, b| a <=> b }
37 |
38 | res = TextMate::UI.request_item(
39 | :title => "Categories",
40 | :prompt => "Choose categories to insert:",
41 | :items => names,
42 | :button1 => 'Insert'
43 | )
44 |
45 | TextMate.exit_discard if res.nil?
46 | puts res.gsub(/([^,]+),?\s*/, "Category: \\1\n")
47 | end # select_cats
48 |
49 | cred = Blogging.new
50 | endpoint = cred.endpoint
51 | username = cred.username
52 | password = cred.password
53 | res = TextMate.call_with_progress(:title => "Fetch Categories", :message => "Contacting Server “#{cred.host}”…") do
54 | cred.client.call("metaWeblog.getCategories", endpoint, username, password)
55 | end
56 |
57 | TextMate.exit_show_tool_tip "No categories are available!" if res.nil? || res.empty?
58 | select_from_cats(res)
59 |
60 | input
61 | document
62 | name
63 | Category
64 | output
65 | afterSelectedText
66 | scope
67 | text.blog, text.blog text.html.markdown, text.blog text.plain, text.blog text.html.textile, text.blog text.html
68 | tabTrigger
69 | cat
70 | uuid
71 | 40318F5D-111F-4451-BBB0-F282DEAC881F
72 |
73 |
74 |
--------------------------------------------------------------------------------
/Support/lib/browser.rb:
--------------------------------------------------------------------------------
1 | require "#{ENV['TM_SUPPORT_PATH']}/lib/osx/plist"
2 |
3 | require 'uri'
4 |
5 | module Browser
6 | class << self
7 | def load_url(url)
8 | url = URI.parse(url.to_s)
9 | return if url.scheme.nil?
10 | browsers = [
11 | { :name => "Camino", :id => "org.mozilla.camino" },
12 | { :name => "OmniWeb", :id => "com.omnigroup.omniweb5" },
13 | { :name => "Safari", :id => "com.apple.safari" },
14 | { :name => "Safari", :id => "org.webkit.nightly.webkit" },
15 | ]
16 |
17 | fav = favorite.to_s.downcase
18 | browsers.each do |browser|
19 | if fav == browser[:id] && (%x{ps -xc|grep -qs #{browser[:name]}}; $?.exitstatus == 0) then
20 | return if self.send(browser[:id].tr('.', '_') + '_did_load?', url)
21 | end
22 | end
23 |
24 | %x{open '#{url}'}
25 | end
26 |
27 | def favorite
28 | rec = nil
29 | open(File.expand_path("~/Library/Preferences/com.apple.LaunchServices.plist")) do |io|
30 | rec = OSX::PropertyList.load(io)["LSHandlers"].find { |info| info["LSHandlerURLScheme"] == "http" }
31 | end
32 | rescue
33 | ensure
34 | return rec ? rec["LSHandlerRoleAll"] : nil
35 | end
36 |
37 | def org_mozilla_camino_did_load?(url)
38 | %x{osascript <<'APPLESCRIPT'
39 | tell app "Camino"
40 | set theWindows to windows where visible is true
41 | if theWindows is not { }
42 | set the_url to URL of (first item of theWindows)
43 | if the_url is "#{url}" then
44 | activate
45 | tell app "System Events" to keystroke "r" using {command down}
46 | return true
47 | end if
48 | end if
49 | end tell
50 | APPLESCRIPT} =~ /true/
51 | end
52 |
53 | def com_omnigroup_omniweb5_did_load?(url)
54 | %x{osascript <<'APPLESCRIPT'
55 | tell app "OmniWeb"
56 | if browsers is not { }
57 | set the_url to address of first browser
58 | if the_url is "#{url}" then
59 | activate
60 | tell app "System Events" to keystroke "r" using {command down}
61 | return true
62 | end if
63 | end if
64 | end tell
65 | APPLESCRIPT} =~ /true/
66 | end
67 |
68 | def com_apple_safari_did_load?(url)
69 | %x{osascript <<'APPLESCRIPT'
70 | tell app "Safari"
71 | if documents is not { }
72 | set the_url to URL of first document
73 | if the_url is "#{url}" then
74 | activate
75 | do JavaScript "window.location.reload();" in first document
76 | return true
77 | end if
78 | end if
79 | end tell
80 | APPLESCRIPT} =~ /true/
81 | end
82 |
83 | def org_webkit_nightly_webkit_did_load?(url)
84 | %x{osascript <<'APPLESCRIPT'
85 | tell app "WebKit"
86 | if documents is not { }
87 | set the_url to URL of first document
88 | if the_url is "#{url}" then
89 | activate
90 | do JavaScript "window.location.reload();" in first document
91 | return true
92 | end if
93 | end if
94 | end tell
95 | APPLESCRIPT} =~ /true/
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | contactEmailRot13
6 | oenq@oenqpubngr.pbz
7 | contactName
8 | Brad Choate
9 | deleted
10 |
11 | 44FCEFFC-8F21-4BC4-BBA2-33D79367EE29
12 |
13 | description
14 | <a href="http://macromates.com/blog/archives/2006/06/19/blogging-from-textmate/">Post directly to your blog</a> with this bundle.
15 | mainMenu
16 |
17 | items
18 |
19 | 10CFDE4C-E433-48F8-AB94-17E1FBEFC074
20 | BD6E6210-F4A3-4821-BA43-A9DFF4178704
21 | ------------------------------------
22 | 60853977-B0D2-4776-A3D9-4B6C09E18596
23 | FA5DC73E-AAE0-4C69-86E1-87B9E0390FD0
24 | ------------------------------------
25 | 3E064A5D-EC5C-48F8-B3BC-15F4CD442827
26 | 6F1CF327-E4C8-49D8-BD30-FD6F987AE23A
27 | ------------------------------------
28 | 17B2F39B-5CCB-4B0E-B305-8C27BED56887
29 | 8DCBE1EB-A3CC-4559-872E-34A3643F0BC4
30 |
31 | submenus
32 |
33 | 3E064A5D-EC5C-48F8-B3BC-15F4CD442827
34 |
35 | items
36 |
37 | C316CEDC-A4E1-41DE-B102-CDF78845ACF3
38 | 40318F5D-111F-4451-BBB0-F282DEAC881F
39 | 6CEE1440-987D-4E37-BA50-9AD23784D2E5
40 | B55BA9A1-0F22-45F4-B7C8-B216BF038AF9
41 | 5CCBC664-2477-4C8C-8B8B-7B57DB15DB23
42 | 853BB979-1DA2-4D78-B50D-092B80A1877F
43 | 2238B6A6-0961-4DC6-917D-477F776CAF9D
44 | D947660B-DD37-44C3-B4A5-7003B90CD0DD
45 | 5C0A6073-6592-4841-83A0-C03D742074E5
46 |
47 | name
48 | Headers
49 |
50 |
51 |
52 | name
53 | Blogging
54 | ordering
55 |
56 | 10CFDE4C-E433-48F8-AB94-17E1FBEFC074
57 | BD6E6210-F4A3-4821-BA43-A9DFF4178704
58 | 60853977-B0D2-4776-A3D9-4B6C09E18596
59 | FA5DC73E-AAE0-4C69-86E1-87B9E0390FD0
60 | 17B2F39B-5CCB-4B0E-B305-8C27BED56887
61 | 8DCBE1EB-A3CC-4559-872E-34A3643F0BC4
62 | C316CEDC-A4E1-41DE-B102-CDF78845ACF3
63 | 40318F5D-111F-4451-BBB0-F282DEAC881F
64 | 6CEE1440-987D-4E37-BA50-9AD23784D2E5
65 | 6F1CF327-E4C8-49D8-BD30-FD6F987AE23A
66 | B55BA9A1-0F22-45F4-B7C8-B216BF038AF9
67 | 5CCBC664-2477-4C8C-8B8B-7B57DB15DB23
68 | 853BB979-1DA2-4D78-B50D-092B80A1877F
69 | 2238B6A6-0961-4DC6-917D-477F776CAF9D
70 | D947660B-DD37-44C3-B4A5-7003B90CD0DD
71 | 5C0A6073-6592-4841-83A0-C03D742074E5
72 | 6AA68B5B-18B8-4922-9CED-0E2295582955
73 | 32E65853-CDBD-401A-ADBE-F94F195249BE
74 | E46F5C50-5D16-4B5C-8FBB-686BD3768284
75 | B2CCDFF8-0FB3-492A-8761-D31FF0FAC448
76 | 7A2373C1-A300-49B5-9F83-676612D8D5B5
77 | F0B36FAE-07A0-46C9-992F-5901327FE266
78 | 60F07B75-41FD-473F-A390-9E821D880469
79 | E7B58845-506D-4065-9835-0D37DCFC02D2
80 | 96F84D65-3CD5-4088-9360-15131E1611DF
81 |
82 | uuid
83 | 79741B2E-271D-4CBC-A61A-380C83D36863
84 |
85 |
86 |
--------------------------------------------------------------------------------
/Support/help.markdown:
--------------------------------------------------------------------------------
1 | Getting Started
2 | ===============
3 |
4 | You should first use the "Setup Blogs" command to identify any blogs you wish to post to. This command will load a file for editing. Within this file, you simply specify a blog name and the XMLRPC URL for it. For example:
5 |
6 | My MT Blog http://myusername@mydomain.com/mt/mt-xmlrpc.cgi#1
7 | My WordPress Blog http://myusername@mydomain.com/blog/xmlrpc.php
8 | My Typo Blog http://myusername@mydomain.com/backend/xmlrpc
9 |
10 | After you've configured for your blog(s), try the "Fetch Post" command to retrieve an existing post for editing. This is the easiest way to confirm that your configuration is correct. You will be prompted for your password upon initially connecting to your blog. Once you have that working, you should have no problem publishing a new post.
11 |
12 | To create a new blog entry, create a new document from one of the Blogging templates:
13 |
14 | * Blog Post (Markdown)
15 | * Blog Post (Textile)
16 | * Blog Post (HTML)
17 | * Blog Post (Text)
18 |
19 | Each of these will give you a basic heading section followed with the text of the post itself. Additional headers are defined as snippets within this bundle. Once you've composed your post, use the "Post to Blog" command to publish it. Upon a successful post (and assuming you elected to publish the post, instead of setting it to a draft state), your browser will open to the published page. The document window you were working in will also be refreshed with the post as retrieved from your blog (which will add other missing headers, such as the post ID).
20 |
21 | Feel free to customize these templates to suit your preferences.
22 |
23 | Headers
24 | =======
25 |
26 | To specify all the relevant metadata along with your post, add one of the following items to the top of the document:
27 |
28 | * Title - The title for your entry.
29 | * Post - The ID of the blog entry (this is meant to be assigned by the server; you shouldn't have to enter this yourself).
30 | * Date - The date of the post. Omit this header to post with the current time.
31 | * Category - One or more headers to specify the categories for the entry.
32 | * Ping - One or more TrackBack URLs you wish to ping.
33 | * Keywords - A list of keywords to associate with the entry.
34 | * Tags - A list of tags to assign to the entry.
35 | * Pings - "On" or "Off"; used to control whether the entry accepts TrackBacks.
36 | * Comments - "On" or "Off"; used to control whether the entry accepts comments.
37 | * Format - Typically server assigned; the format of the template you're using should control this.
38 | * Blog - An XMLRPC endpoint URL or blog identifier (from your blog accounts file).
39 |
40 | Header lines are written with the heading keyword followed with a colon (":") and then the value for the keyword follows.
41 |
42 | Title: Title of the post
43 |
44 | Certain headers may be repeated, allowing you to assign your post to multiple categories (with the "Category" header) or to ping multiple blogs (with the "Ping" header).
45 |
46 | All of these headers are optional. If you don't specify any of them, they will typically be assigned a default value by your blog software. The "Title" field is the only thing that will be requested if it is omitted. If you try to post an entry without a title, you will be asked for a title. However when prompted, you may still leave it blank if you wish.
47 |
48 | The "Date" header must be specified with this format: YYYY-MM-DD HH:MM:SS. You may omit the time, but it will be replaced with 12 AM if you do so. Leaving the date header out altogether will cause your entry to be posted with the current time.
49 |
50 | Your blog software may not support some of these headers. I recommend that you issue a Fetch command to retrieve an existing post and take note of the headers returned with the post. That will give you an indication of which headers are supported for new posts.
51 |
52 | Commands
53 | ========
54 |
55 | * **Post to Blog**: Takes a new or edited post and publishes it to your configured blog.
56 | * **Fetch Post**: Selects the last available posts (up to 20) and lets you choose one for editing. This will create a new document locally that you can edit and then republish using the Post to Blog command.
57 | * **View Online Version**: For a published post (one with a Post ID within the document), this command will take you to the permalink for that post.
58 | * **Setup Blogs**: Loads the blog accounts file for editing the list of blogs that you want to publish to.
59 |
60 | Image Uploads
61 | =============
62 |
63 | It is possible to upload an image to your blog endpoint by dragging it into the document. After successful upload the URL of the image will be inserted.
64 |
65 | By default you will be asked for the description of the image (used as argument for the `alt` attribute) and a “safe” file name will be derived from this description (i.e. lowercased using only alphanumeric characters and no spaces).
66 |
67 | If you instead wish to provide the actual file name under which the image should be uploaded then you can hold down option (⌥) when dragging it into your document.
68 |
69 | Environment Variables
70 | =====================
71 |
72 | The following settings are available, but optional. They can either be specified in TextMate's global Shell Variables or within a specific project.
73 |
74 | * `TM_BLOG_ENDPOINT`: You may either specify a named blog endpoint (as configured with the "Setup Blogs" command) or an endpoint URL. This value can be overriden by a "Blog" header within a post. If this is unset **and** your document doesn't have a "Blog" header, you will be prompted for which blog to use.
75 | * `TM_BLOG_FORMAT`: Your preferred formatting choice (default will derive from current blog template selected).
76 | * `TM_BLOG_MODE`: 'mt' and 'wp' are valid settings for this. Influences the posting API to be as compatible as possible with Movable Type, TypePad, Blogger, Typo and WordPress variants of the metaWeblog API (default will derive from endpoint URL, or will default to 'mt').
77 | * `TM_HTTP_PROXY`: If you are behind a proxy set it to `host:port` of the proxy.
78 | * `TM_BLOG_POST_COUNT`: Controls the number of posts retrieved by the "Fetch Post" command. The default is 100.
79 |
80 |
81 | Troubleshooting
82 | ===============
83 |
84 | * If you change your blog password, you will need to also change your local password. The password is stored in your KeyChain, so you'll have to search for it and update it or delete the Keychain record to be prompted to reauthenticate.
85 |
86 | Credits
87 | =======
88 |
89 | This bundle is maintained by [Brad Choate][1].
90 |
91 | [1]: http://bradchoate.com/
92 |
--------------------------------------------------------------------------------
/Support/lib/blogging.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | require 'cgi'
4 | require "#{ENV['TM_SUPPORT_PATH']}/lib/exit_codes.rb"
5 | require "#{ENV['TM_SUPPORT_PATH']}/lib/escape.rb"
6 | require "#{ENV['TM_SUPPORT_PATH']}/lib/ui.rb"
7 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/keychain.rb"
8 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/metaweblog.rb"
9 |
10 | # FIXME: this will need work. DT
11 | $KCODE = 'u'
12 |
13 | class Blogging
14 | DIVIDER = '✂------'
15 | BLOG_ACCOUNTS_FILE = File.expand_path("~/Library/Preferences/com.macromates.textmate.blogging.txt")
16 |
17 | # These elements are gathered from the environment; no need to explicitly
18 | # write to them.
19 | attr_reader :blog_id, :username, :host, :path, :mode, :headers, :publish
20 |
21 | # These elements can be assigned through public methods
22 | attr_writer :post_id, :post, :endpoint
23 |
24 | private
25 |
26 | def initialize
27 | at_exit { finalize() }
28 | end
29 |
30 | def finalize
31 | if @mw_success
32 | # If an internet password was gathered during this process, and our
33 | # connection was successful, save it for the next request.
34 | finally_save_internet_password() if @save_password_on_success
35 |
36 | # If an endpoint was used for the first time during this command, and
37 | # the connection to it was successful, save it for the next request.
38 | finally_save_new_endpoint() if @save_endpoint_on_success
39 | end
40 | end
41 |
42 | def finally_save_internet_password
43 | protocol = self.endpoint =~ /^https:/ ? 'htps' : 'http'
44 | endpoint_path = self.path.dup
45 | endpoint_path.sub!(/#.+/, '') if endpoint_path =~ /#.+/
46 | KeyChain.add_internet_password(self.username, protocol, self.host,
47 | endpoint_path, self.password)
48 | end
49 |
50 | def finally_save_new_endpoint
51 | if File.exist?(BLOG_ACCOUNTS_FILE)
52 | endpoint_list = IO.readlines(BLOG_ACCOUNTS_FILE)
53 | else
54 | endpoint_list = [ <<-TEXT ]
55 | # Blogging Account List
56 | # Enter a blog name followed by the endpoint URL (see Help for proxy config)
57 | # Blog Name URL
58 | # example http://user@example.com/xmlrpc
59 | TEXT
60 | end
61 | endpoint_list.push(self.endpoints[self.endpoint] + " " + self.endpoint +
62 | "\n")
63 | File.open(BLOG_ACCOUNTS_FILE, "w") do | file |
64 | file.write(endpoint_list.join)
65 | end
66 | end
67 |
68 | def find_internet_password
69 | protocol = self.endpoint =~ /^https:/ ? 'htps' : 'http'
70 | endpoint_path = self.path.dup
71 | endpoint_path.sub!(/#.+/, '') if endpoint_path =~ /#.+/
72 | KeyChain.find_internet_password(self.username, protocol, self.host,
73 | endpoint_path)
74 | end
75 |
76 | def fetch_credentials_from_keychain
77 | # we have @endpoint and possibly @username. fill in the blanks...
78 | if @username == nil
79 | @username = TextMate::UI.request_string(
80 | :title => "Blog Username",
81 | :prompt => "Enter the username to login at #{self.endpoint}:",
82 | :button1 => 'Login'
83 | )
84 | TextMate.exit_discard if self.username == nil
85 | end
86 |
87 | if @password == nil
88 | @password = find_internet_password()
89 | if @password == nil
90 | current_endpoint = self.endpoint.dup
91 | current_endpoint.sub!(/#.+/, '') if current_endpoint =~ /#.+/
92 | @password = TextMate::UI.request_secure_string(
93 | :title => "Blog Password",
94 | :prompt => "Enter the password to login at #{current_endpoint}:",
95 | :button1 => 'Login'
96 | )
97 | TextMate.exit_discard if @password == nil
98 | @save_password_on_success = true
99 | end
100 | end
101 | end
102 |
103 | def read_endpoints
104 | @endpoints = {}
105 | if File.exist?(BLOG_ACCOUNTS_FILE)
106 | IO.readlines(BLOG_ACCOUNTS_FILE).each do | line |
107 | next if line =~ /^\s*#/
108 | if line =~ /^(.+?)\s+(https?:\/\/.+)/
109 | @endpoints[$1] = $2
110 | @endpoints[$2] = $1
111 | end
112 | end
113 | end
114 | @endpoints
115 | end
116 |
117 | def parse_endpoint
118 | # we have an endpoint that looks like a URL
119 | if @endpoint =~ /^https?:\/\//
120 | if self.endpoints[@endpoint]
121 | # The endpoint is a recognized URL; nothing else to do
122 | else
123 | # the endpoint is a URL but unrecognized... ask for a pretty name
124 | name = TextMate::UI.request_string(
125 | :title => "Endpoint Name",
126 | :prompt => "Enter a name for: #{@endpoint}",
127 | :button1 => 'OK'
128 | )
129 | if name != nil
130 | self.endpoints[name] = @endpoint
131 | self.endpoints[@endpoint] = name
132 | @save_endpoint_on_success = true
133 | else
134 | TextMate.exit_discard
135 | end
136 | end
137 | else
138 | if !self.endpoints[@endpoint]
139 | url = TextMate::UI.request_string(
140 | :title => "Endpoint URL",
141 | :prompt => "Enter an endpoint URL for blog #{@endpoint}",
142 | :button1 => 'OK'
143 | )
144 | if url != nil
145 | self.endpoints[@endpoint] = url
146 | self.endpoints[url] = @endpoint
147 | @endpoint = url
148 | @save_endpoint_on_success = true
149 | else
150 | TextMate.exit_discard
151 | end
152 | else
153 | # we had a named endpoint; swap with the URL...
154 | @endpoint = self.endpoints[@endpoint]
155 | end
156 | end
157 |
158 | # guess the mode based on the endpoint path
159 | @mode = ENV['TM_BLOG_MODE']
160 | if @mode == nil
161 | case @endpoint
162 | when %r{/mt-xmlrpc\.cgi}, %r{/backend/xmlrpc}
163 | @mode = 'mt'
164 | when %r{/serendipity_xmlrpc\.php}
165 | @mode = 's9y'
166 | when %r{/xmlrpc(\.php)?}
167 | @mode = 'wp'
168 | else
169 | # our default
170 | @mode = 'mt'
171 | end
172 | end
173 |
174 | if @endpoint =~ /^https?:\/\/([^\/]+?)(\/.+)$/
175 | @host = $1
176 | @path = $2
177 | if @host =~ /^([^:]+)(?:[:](.+))?@(.+)/
178 | @username = CGI.unescape($1)
179 | @password = CGI.unescape($2) if $2
180 | @host = $3
181 | end
182 | else
183 | TextMate.exit_show_tool_tip("Error: invalid endpoint specified: #{@endpoint}")
184 | end
185 |
186 | if @endpoint =~ /#(.+)/
187 | @blog_id = $1
188 | else
189 | @blog_id = "0"
190 | end
191 | end
192 |
193 | def parse_post
194 | lines = STDIN.readlines
195 |
196 | @post = {}
197 | @headers = {}
198 |
199 | return if lines.length == 0
200 |
201 | @post['mt_text_more'] = ''
202 | @post['description'] = ''
203 | @publish = true
204 |
205 | in_headers = true
206 | separator = false
207 |
208 | lines.each do | line |
209 | if in_headers
210 | if line =~ /^(\w+):[ ]*(.+)/
211 | key = $1.downcase
212 | case key
213 | when 'ping'
214 | @post['mt_tb_ping_urls'] = [] unless @post['mt_tb_ping_urls']
215 | @post['mt_tb_ping_urls'].push($2)
216 | when 'category'
217 | @post['categories'] = [] unless @post['categories']
218 | @post['categories'].push($2)
219 | when 'status'
220 | @publish = false if $2 =~ /draft/i
221 | end
222 | @headers[key] = $2
223 | next
224 | else
225 | in_headers = false
226 | end
227 | end
228 | if line =~ %r{^✂-[✂-]+}
229 | if !separator
230 | separator = true
231 | next
232 | else
233 | # establish endpoint, which sets mode
234 | current_endpoint = self.endpoint
235 | if self.mode == 'wp'
236 | line = ''
237 | end
238 | end
239 | end
240 | @post[ separator ? 'mt_text_more' : 'description' ] << line
241 | end
242 |
243 | @post['description'].strip!
244 | @post['mt_text_more'].strip!
245 |
246 | if (@post['mt_text_more'] == '') || (@post['mt_text_more'] == nil)
247 | @post.delete('mt_text_more')
248 | else
249 | @post['description'] << "\n\n"
250 | @post['mt_text_more'] = "\n" + @post['mt_text_more']
251 | end
252 | @post['title'] = @headers['title'] if @headers['title']
253 | @post['wp_slug'] = @headers['slug'] if @headers['slug']
254 | @post['wp_post_format'] = @headers['postformat'] if @headers['postformat']
255 | self.post_id = @headers['post'] if @headers['post']
256 |
257 | format = @headers['format']
258 | self.post['mt_convert_breaks'] = format if format
259 | if !format
260 | # we have to parse endpoint before
261 | # examining the mode variable, since it can be assigned by
262 | # the format of the endpoint url
263 | current_endpoint = self.endpoint
264 |
265 | # scope-based sniffing of format; these are MT-specific.
266 | if self.mode == 'mt'
267 | case ENV['TM_SCOPE']
268 | when /markdown/
269 | self.post['mt_convert_breaks'] = 'markdown_with_smartypants'
270 | when /textile/
271 | self.post['mt_convert_breaks'] = 'textile_2'
272 | when /text\.blog\.html/
273 | self.post['mt_convert_breaks'] = '0'
274 | else
275 | self.post['mt_convert_breaks'] = '__default__'
276 | end
277 | end
278 | end
279 |
280 | date_created = DateTime.parse(@headers['date']) if @headers['date']
281 | if date_created && self.mode != 'mt' then
282 | # We manually create an XMLRPC:DateTime object because
283 | # xmlrpc/create.rb (prior to 2007-02-23 = Tiger version)
284 | # will not include the time part of the DateTime object
285 | d = date_created
286 | @post['dateCreated'] = XMLRPC::DateTime.new(d.year, d.mon, d.day, d.hour, d.min, d.sec)
287 | d = date_created.new_offset(0)
288 | @post['date_created_gmt'] = XMLRPC::DateTime.new(d.year, d.mon, d.day, d.hour, d.min, d.sec)
289 | end
290 |
291 | if self.mode == 'mt'
292 | @post['dateCreated'] = date_created.strftime('%FT%T') if date_created
293 | @post['mt_allow_comments'] = @headers['comments'] =~ /\b(on|1|y(es)?)\b/i ? '1' : '0' if @headers['comments']
294 | @post['mt_allow_pings'] = @headers['pings'] =~ /\b(on|1|y(es)?)\b/i ? '1' : '0' if @headers['pings']
295 | @post['mt_tags'] = @headers['tags'] if @headers['tags']
296 | @post['mt_basename'] = @headers['basename'] if @headers['basename']
297 | elsif self.mode == 's9y'
298 | @post['mt_allow_comments'] = @headers['comments'] =~ /\b(on|1|y(es)?)\b/i ? '1' : '0' if @headers['comments']
299 | @post['mt_allow_pings'] = @headers['pings'] =~ /\b(on|1|y(es)?)\b/i ? '1' : '0' if @headers['pings']
300 | elsif self.mode == 'wp'
301 | @post['mt_allow_comments'] = @headers['comments'] =~ /\b(on|1|y(es)?)\b/i ? 'open' : 'closed' if @headers['comments']
302 | @post['mt_allow_pings'] = @headers['pings'] =~ /\b(on|1|y(es)?)\b/i ? 'open' : 'closed' if @headers['pings']
303 | end
304 | @post['mt_keywords'] = @headers['keywords'] if @headers['keywords']
305 | end
306 |
307 | public
308 |
309 | # Getters/Setters
310 |
311 | def password
312 | # The password can be embedded within the endpoint, so resolve
313 | # the endpoint first, which may set @password for us.
314 | self.endpoint
315 | fetch_credentials_from_keychain() unless @password
316 | @password
317 | end
318 |
319 | def post
320 | parse_post() if @post == nil
321 | @post
322 | end
323 |
324 | def post=(new_post)
325 | @post = new_post
326 | @post_id = self.post['postid'] if self.post['postid']
327 | end
328 |
329 | def post_id
330 | parse_post() if @post == nil
331 | @post_id
332 | end
333 |
334 | def publish
335 | parse_post() if @post == nil
336 | @publish
337 | end
338 |
339 | def headers
340 | parse_post() if @post == nil
341 | @headers
342 | end
343 |
344 | def endpoint=(new_endpoint)
345 | @endpoint = new_endpoint
346 | parse_endpoint()
347 | end
348 |
349 | def endpoint
350 | return @endpoint if @endpoint != nil
351 |
352 | current_endpoint = nil
353 |
354 | # Check the headers for a 'Blog' which is an endpoint
355 | current_endpoint = self.headers['blog']
356 |
357 | # Check TM_BLOG_ENDPOINT as a fallback
358 | current_endpoint ||= ENV['TM_BLOG_ENDPOINT']
359 |
360 | # Still no luck? Ask the user using endpoints in their config.
361 | current_endpoint ||= select_endpoint()
362 |
363 | TextMate.exit_discard if current_endpoint.nil?
364 |
365 | self.endpoint = current_endpoint
366 | end
367 |
368 | def endpoints
369 | read_endpoints() unless @endpoints
370 | @endpoints
371 | end
372 |
373 | def client
374 | current_endpoint = endpoint.dup
375 | current_endpoint.sub!(/#.+/, '') if current_endpoint =~ /#.+/
376 | @client ||= MetaWeblogClient.new2(current_endpoint, ENV['TM_HTTP_PROXY'])
377 | @client
378 | end
379 |
380 | # Utility methods
381 |
382 | def post_to_document
383 | doc = ''
384 | formats = {
385 | 'textile_1' => 'Textile',
386 | 'textile_2' => 'Textile',
387 | 'markdown_with_smartypants' => 'Markdown',
388 | 'markdown' => 'Markdown',
389 | 'textile' => 'Textile',
390 | 'Textile' => 'Textile',
391 | 'Markdown' => 'Markdown',
392 | '__default__' => 'Text',
393 | }
394 |
395 | format = 'Markdown'
396 | format = 'HTML' if "#{self.post['description']}#{self.post['mt_text_more']}" =~
397 | /<(p|a|img|h[1-6]|strong|em|tt|code|pre)\b.*?>/i
398 |
399 | if post['mt_convert_breaks']
400 | if formats[self.post['mt_convert_breaks']]
401 | format = formats[self.post['mt_convert_breaks']]
402 | end
403 | elsif ENV['TM_BLOG_FORMAT']
404 | format = ENV['TM_BLOG_FORMAT']
405 | else
406 | # derive format from existing scope
407 | case ENV['TM_SCOPE']
408 | when /\.markdown/
409 | format = "Markdown"
410 | when /\.textile/
411 | format = "Textile"
412 | when /text\.blog\.html/
413 | format = "HTML"
414 | when /text\.blog\.plain/
415 | format = "Text"
416 | end
417 | end
418 |
419 | blog = self.endpoints[self.endpoint] || self.endpoint
420 | doc << "Type: Blog Post (#{format})\n"
421 | doc << "Blog: #{blog}\n"
422 | doc << "Link: #{self.post['permaLink']}\n" if self.post['permaLink']
423 | doc << "Post: #{self.post_id}\n"
424 | doc << "Title: #{self.post['title']}\n"
425 | doc << "Slug: #{self.post['wp_slug']}\n" unless self.post['wp_slug'].to_s.empty?
426 | doc << "Postformat: #{self.post['wp_post_format']}\n" unless self.post['wp_post_format'].to_s.empty?
427 | doc << "Keywords: #{self.post['mt_keywords']}\n" unless self.post['mt_keywords'].to_s.empty?
428 | doc << "Tags: #{self.post['mt_tags']}\n" if self.post['mt_tags'] && (self.post['mt_tags'] != '')
429 | doc << "Status: #{self.post['post_status']}\n" if self.post['post_status']
430 | if (self.mode == 'wp') && self.post['category']
431 | cats = self.post['category'].split(/,/)
432 | cats.each { | cat | doc << "Category: #{cat}\n" }
433 | end
434 | doc << "Format: #{self.post['mt_convert_breaks']}\n" if self.post['mt_convert_breaks']
435 |
436 | if self.post.has_key? 'date_created_gmt'
437 | d = DateTime.civil(*self.post['date_created_gmt'].to_a)
438 | d = d.new_offset(DateTime.now.offset)
439 | doc << d.strftime("Date: %F %T %z") + "\n"
440 | elsif self.post.has_key? 'dateCreated'
441 | d = DateTime.civil(*(self.post['dateCreated'].to_a << DateTime.now.offset))
442 | doc << d.strftime("Date: %F %T %z") + "\n"
443 | end
444 |
445 | if self.post['mt_allow_pings'] && (self.post['mt_allow_pings'] == 1)
446 | doc << "Pings: On\n"
447 | else
448 | doc << "Pings: Off\n"
449 | end
450 | if self.post['mt_allow_comments'] && (self.post['mt_allow_comments'] == 1)
451 | doc << "Comments: On\n"
452 | else
453 | doc << "Comments: Off\n"
454 | end
455 | doc << "Basename: " + self.post['mt_basename'] + "\n" if self.post['mt_basename']
456 | if self.post['categories']
457 | self.post['categories'].each do | cat |
458 | doc << "Category: #{cat}\n"
459 | end
460 | end
461 | doc << "\n"
462 | doc << self.post['description'].to_s.strip + "\n"
463 | unless self.post['mt_text_more'].nil?
464 | if (more = self.post['mt_text_more'].strip) && more != ''
465 | doc << "\n#{DIVIDER * 10}\n\n"
466 | if (self.mode == 'wp')
467 | more.gsub!('', DIVIDER * 10)
468 | end
469 | doc << more + "\n"
470 | end
471 | end
472 | doc
473 | end
474 |
475 | def request_title(default)
476 | result = TextMate::UI.request_string(
477 | :title => "Post Title",
478 | :default => default,
479 | :prompt => "Enter a title for this post:",
480 | :button1 => 'Post'
481 | )
482 | TextMate.exit_discard if result.nil?
483 | result
484 | end
485 |
486 | def show_post_page
487 | begin
488 | current_password = self.password
489 | self.post = client.getPost(self.post_id, self.username, current_password)
490 | if self.publish && link = self.post['permaLink']
491 | require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/browser"
492 | Browser.load_url(link)
493 | end
494 | @mw_success = true
495 | rescue XMLRPC::FaultException => e
496 | TextMate.exit_show_tool_tip("Error: #{e.faultString} (#{e.faultCode})")
497 | end
498 | end
499 |
500 | def select_post(posts)
501 | titles = posts.map { |p| p['title'] }
502 |
503 | result = TextMate::UI.request_item(
504 | :title => "Fetch Post",
505 | :prompt => "Select a recent post to edit:",
506 | :items => titles,
507 | :button1 => 'Load'
508 | )
509 |
510 | return nil if result.nil?
511 | return posts[titles.index(result)]
512 | end
513 |
514 | def select_endpoint
515 | if self.endpoints.length == 2
516 | # there's only one endpoint here (we store two keys for each)
517 | # return the first one
518 | self.endpoints.each_key do | name |
519 | return name if name !~ /^https?:/
520 | return self.endpoints[name]
521 | end
522 | end
523 |
524 | titles = []
525 | self.endpoints.each_key do | name |
526 | titles << name unless name =~ /^https?:/
527 | end
528 |
529 | if titles.length == 0
530 | TextMate.exit_show_tool_tip("No blog accounts are configured.\nPlease see Help or run Setup Blogs command.")
531 | end
532 |
533 | titles.sort!
534 | result = TextMate::UI.request_item(
535 | :title => "Choose Blog",
536 | :prompt => "Choose a blog:",
537 | :items => titles,
538 | :button1 => 'Choose'
539 | )
540 |
541 | return self.endpoints[result]
542 | end
543 |
544 | # Command: Post
545 |
546 | def post_or_update
547 | if !post['title']
548 | filename = ENV['TM_FILENAME'].to_s.sub(/(\.[a-z]+)+$/, '')
549 | self.post['title'] = request_title(filename)
550 | end
551 |
552 | current_password = self.password
553 | require "#{ENV['TM_SUPPORT_PATH']}/lib/progress.rb"
554 | TextMate.call_with_progress(:title => "Posting to Blog", :message => "Contacting Server “#{@host}”…") do
555 | begin
556 | if post_id
557 | result = client.editPost(self.post_id, self.username, current_password, self.post, self.publish)
558 | else
559 | self.post_id = client.newPost(self.blog_id, self.username, current_password, self.post, self.publish)
560 | end
561 | rescue XMLRPC::FaultException => e
562 | TextMate.exit_show_tool_tip("Error: #{e.faultString} (#{e.faultCode})")
563 | end
564 | show_post_page()
565 | end
566 | @mw_success = true
567 | TextMate.exit_replace_document(post_to_document())
568 | end
569 |
570 | # Command: Fetch
571 |
572 | def fetch
573 | # Makes sure endpoint is determined and elements are parsed
574 | current_password = self.password
575 | require "#{ENV['TM_SUPPORT_PATH']}/lib/progress.rb"
576 | result = nil
577 | TextMate.call_with_progress(:title => "Fetch Post", :message => "Contacting Server “#{@host}”…") do
578 | begin
579 | result = self.client.getRecentPosts(self.blog_id, self.username, current_password, ENV['TM_BLOG_POST_COUNT'] || 100)
580 | rescue XMLRPC::FaultException => e
581 | TextMate.exit_show_tool_tip("Error: #{e.faultString} (#{e.faultCode})")
582 | end
583 | end
584 | if !result || !result.length
585 | TextMate.exit_show_tool_tip("No posts are available!")
586 | end
587 | @mw_success = true
588 | if self.post = select_post(result)
589 | TextMate.exit_create_new_document(post_to_document())
590 | else
591 | TextMate.exit_discard
592 | end
593 | end
594 |
595 | # Command: View
596 |
597 | def view
598 | if self.post_id
599 | show_post_page()
600 | else
601 | TextMate.exit_show_tool_tip("A Post ID is required to view the post.")
602 | end
603 | end
604 |
605 | # 'blog' Command (snippet)
606 |
607 | def choose_blog_endpoint
608 | if self.endpoints.length == 0
609 | TextMate.exit_show_tool_tip(%Q{No blogs have been configured.\n} +
610 | %q{Use the "Setup Blogs" command."})
611 | end
612 |
613 | if self.endpoints.length == 2
614 | current_endpoint = nil
615 | self.endpoints.each_key do | name |
616 | if name !~ /^https?:/
617 | current_endpoint = name
618 | else
619 | current_endpoint = self.endpoints[name]
620 | end
621 | break
622 | end
623 | TextMate.exit_insert_snippet("Blog: #{current_endpoint}")
624 | end
625 |
626 | # TBD: preserve order from endpoint file
627 | titles = []
628 | self.endpoints.each_key do | name |
629 | next if name =~ /^https?:/
630 | titles.push(name)
631 | end
632 | titles.sort!
633 |
634 | require "#{ENV['TM_SUPPORT_PATH']}/lib/ui.rb"
635 | opt = TextMate::UI.menu(titles)
636 |
637 | if opt != nil
638 | TextMate.exit_insert_snippet("Blog: " + titles[opt] + '$0')
639 | end
640 | end
641 |
642 | def to_html
643 | # endpoint doesn't matter here so set to something bogus
644 | # to prevent TM from asking for one...
645 | @endpoint = 'x'
646 | format = ENV['TM_SCOPE']
647 | doc = "#{self.post['description']}"
648 | doc << "#{self.post['mt_text_more']}" if self.post['mt_text_more']
649 | if self.headers['link']
650 | base = %Q{}
651 | elsif ENV['TM_FILEPATH']
652 | filepath = ENV['TM_FILEPATH'].dup
653 | filepath.gsub!(/ /, '%20')
654 | base = %Q{}
655 | end
656 | html = `. "${TM_SUPPORT_PATH}/lib/webpreview.sh"; html_header Preview Blogging`
657 | case format
658 | when /\.textile/
659 | require ENV['TM_BUNDLE_SUPPORT'] + '/lib/redcloth'
660 | html << RedCloth.new(doc).to_html
661 | when /\.markdown/
662 | require "#{ENV['TM_SUPPORT_PATH']}/lib/tm/markdown"
663 | html << TextMate::Markdown.to_html(doc)
664 | when /\.html/
665 | html << doc
666 | when /\.text/
667 | html << %Q{
#{doc}
}
668 | end
669 | html << `. "${TM_SUPPORT_PATH}/lib/webpreview.sh"; html_footer`
670 | html
671 | end
672 |
673 | # Command: Preview
674 |
675 | def preview
676 | print to_html()
677 | end
678 |
679 | # Drag Command: Upload Image
680 |
681 | def upload_name_for_path(full_path)
682 | require "#{ENV['TM_SUPPORT_PATH']}/lib/escape.rb"
683 |
684 | # WordPress automatically places files into dated paths
685 | prefix = mode == 'wp' ? '' : Time.now.strftime('%F_')
686 | file = File.basename(full_path)
687 |
688 | if ENV['TM_MODIFIER_FLAGS'] =~ /OPTION/
689 |
690 | suggested_name = prefix + file.gsub(/[ ]+/, '-')
691 | result = TextMate::UI.request_string(
692 | :title => "Upload Image",
693 | :default => suggested_name,
694 | :prompt => "Name to use for uploaded file:",
695 | :button1 => 'Upload'
696 | )
697 | TextMate.exit_discard if result.nil?
698 |
699 | alt = result.sub(/\.[^.]+\z/, '').gsub(/[_-]/, ' ').capitalize.gsub(/\w{4,}/) { |m| m.capitalize }
700 | [ result, alt ]
701 |
702 | else
703 |
704 | base = file.sub(/\.[^.]+\z/, '')
705 | ext = file[(base.length)..-1]
706 | suggested_alt = base.gsub(/[_-]/, ' ').gsub(/[a-z](?=[A-Z0-9])/, '\0 ').capitalize.gsub(/\w{4,}/) { |m| m.capitalize }
707 |
708 | result = TextMate::UI.request_string(
709 | :title => "Upload Image",
710 | :default => suggested_alt,
711 | :prompt => "Image description (a filename will be derived from it):",
712 | :button1 => 'Upload'
713 | )
714 | TextMate.exit_discard if result.nil?
715 |
716 | require "iconv"
717 | name = Iconv.new('ASCII//TRANSLIT', 'UTF-8').iconv(result.dup)
718 |
719 | name.gsub!(/[^-_ \/\w]/, '') # remove strange stuff
720 | name.gsub!(/[-_ \/]+/, '_') # collapse word separators into one underscore
721 | name.downcase!
722 | [ prefix + name + ext, result ]
723 |
724 | end
725 | end
726 |
727 | def upload_image
728 | require "#{ENV['TM_SUPPORT_PATH']}/lib/progress.rb"
729 | require "#{ENV['TM_SUPPORT_PATH']}/lib/escape.rb"
730 | require 'xmlrpc/base64'
731 |
732 | # Makes sure endpoint is determined and elements are parsed
733 | current_password = password
734 |
735 | # The packet we will be constructing
736 | data = {}
737 |
738 | full_path = ENV['TM_DROPPED_FILEPATH']
739 | upload_name, alt = upload_name_for_path(full_path)
740 |
741 | data['name'] = upload_name
742 | data['bits'] = XMLRPC::Base64.new(IO.read(full_path))
743 |
744 | TextMate.call_with_progress(:title => "Upload Image", :message => "Uploading to Server “#{@host}”…") do
745 | begin
746 | result = client.newMediaObject(self.blog_id, self.username, current_password, data)
747 | url = result['url']
748 | if url
749 | case ENV['TM_SCOPE']
750 | when /\.markdown/
751 | print ""
752 | when /\.textile/
753 | print "!#{url} (${1:#{alt}})!"
754 | else
755 | height_width = ""
756 | if sips_hw = %x{sips -g pixelWidth -g pixelHeight #{e_sh full_path}}
757 | height = $1 if sips_hw.match(/pixelHeight:[ ]*(\d+)/)
758 | width = $1 if sips_hw.match(/pixelWidth:[ ]*(\d+)/)
759 | if height && width
760 | height_width = %Q{ height="#{height}" width="#{width}"}
761 | end
762 | end
763 | print %Q{
}
764 | end
765 | else
766 | TextMate.exit_show_tool_tip("Error uploading image.")
767 | end
768 | rescue XMLRPC::FaultException => e
769 | TextMate.exit_show_tool_tip("Error uploading image: #{e.faultString} (#{e.faultCode})")
770 | end
771 | end
772 | end
773 |
774 | end
--------------------------------------------------------------------------------
/Support/lib/redcloth.rb:
--------------------------------------------------------------------------------
1 | # vim:ts=4:sw=4:
2 | # = RedCloth - Textile and Markdown Hybrid for Ruby
3 | #
4 | # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
5 | # Author:: why the lucky stiff (http://whytheluckystiff.net/)
6 | # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
7 | # License:: BSD
8 | #
9 | # (see http://hobix.com/textile/ for a Textile Reference.)
10 | #
11 | # Based on (and also inspired by) both:
12 | #
13 | # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
14 | # Textism for PHP: http://www.textism.com/tools/textile/
15 | #
16 | #
17 |
18 | # = RedCloth
19 | #
20 | # RedCloth is a Ruby library for converting Textile and/or Markdown
21 | # into HTML. You can use either format, intermingled or separately.
22 | # You can also extend RedCloth to honor your own custom text stylings.
23 | #
24 | # RedCloth users are encouraged to use Textile if they are generating
25 | # HTML and to use Markdown if others will be viewing the plain text.
26 | #
27 | # == What is Textile?
28 | #
29 | # Textile is a simple formatting style for text
30 | # documents, loosely based on some HTML conventions.
31 | #
32 | # == Sample Textile Text
33 | #
34 | # h2. This is a title
35 | #
36 | # h3. This is a subhead
37 | #
38 | # This is a bit of paragraph.
39 | #
40 | # bq. This is a blockquote.
41 | #
42 | # = Writing Textile
43 | #
44 | # A Textile document consists of paragraphs. Paragraphs
45 | # can be specially formatted by adding a small instruction
46 | # to the beginning of the paragraph.
47 | #
48 | # h[n]. Header of size [n].
49 | # bq. Blockquote.
50 | # # Numeric list.
51 | # * Bulleted list.
52 | #
53 | # == Quick Phrase Modifiers
54 | #
55 | # Quick phrase modifiers are also included, to allow formatting
56 | # of small portions of text within a paragraph.
57 | #
58 | # \_emphasis\_
59 | # \_\_italicized\_\_
60 | # \*strong\*
61 | # \*\*bold\*\*
62 | # ??citation??
63 | # -deleted text-
64 | # +inserted text+
65 | # ^superscript^
66 | # ~subscript~
67 | # @code@
68 | # %(classname)span%
69 | #
70 | # ==notextile== (leave text alone)
71 | #
72 | # == Links
73 | #
74 | # To make a hypertext link, put the link text in "quotation
75 | # marks" followed immediately by a colon and the URL of the link.
76 | #
77 | # Optional: text in (parentheses) following the link text,
78 | # but before the closing quotation mark, will become a Title
79 | # attribute for the link, visible as a tool tip when a cursor is above it.
80 | #
81 | # Example:
82 | #
83 | # "This is a link (This is a title) ":http://www.textism.com
84 | #
85 | # Will become:
86 | #
87 | # This is a link
88 | #
89 | # == Images
90 | #
91 | # To insert an image, put the URL for the image inside exclamation marks.
92 | #
93 | # Optional: text that immediately follows the URL in (parentheses) will
94 | # be used as the Alt text for the image. Images on the web should always
95 | # have descriptive Alt text for the benefit of readers using non-graphical
96 | # browsers.
97 | #
98 | # Optional: place a colon followed by a URL immediately after the
99 | # closing ! to make the image into a link.
100 | #
101 | # Example:
102 | #
103 | # !http://www.textism.com/common/textist.gif(Textist)!
104 | #
105 | # Will become:
106 | #
107 | #
108 | #
109 | # With a link:
110 | #
111 | # !/common/textist.gif(Textist)!:http://textism.com
112 | #
113 | # Will become:
114 | #
115 | #
116 | #
117 | # == Defining Acronyms
118 | #
119 | # HTML allows authors to define acronyms via the tag. The definition appears as a
120 | # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
121 | # this should be used at least once for each acronym in documents where they appear.
122 | #
123 | # To quickly define an acronym in Textile, place the full text in (parentheses)
124 | # immediately following the acronym.
125 | #
126 | # Example:
127 | #
128 | # ACLU(American Civil Liberties Union)
129 | #
130 | # Will become:
131 | #
132 | # ACLU
133 | #
134 | # == Adding Tables
135 | #
136 | # In Textile, simple tables can be added by seperating each column by
137 | # a pipe.
138 | #
139 | # |a|simple|table|row|
140 | # |And|Another|table|row|
141 | #
142 | # Attributes are defined by style definitions in parentheses.
143 | #
144 | # table(border:1px solid black).
145 | # (background:#ddd;color:red). |{}| | | |
146 | #
147 | # == Using RedCloth
148 | #
149 | # RedCloth is simply an extension of the String class, which can handle
150 | # Textile formatting. Use it like a String and output HTML with its
151 | # RedCloth#to_html method.
152 | #
153 | # doc = RedCloth.new "
154 | #
155 | # h2. Test document
156 | #
157 | # Just a simple test."
158 | #
159 | # puts doc.to_html
160 | #
161 | # By default, RedCloth uses both Textile and Markdown formatting, with
162 | # Textile formatting taking precedence. If you want to turn off Markdown
163 | # formatting, to boost speed and limit the processor:
164 | #
165 | # class RedCloth::Textile.new( str )
166 |
167 | class RedCloth < String
168 |
169 | VERSION = '3.0.4'
170 | DEFAULT_RULES = [:textile, :markdown]
171 |
172 | #
173 | # Two accessor for setting security restrictions.
174 | #
175 | # This is a nice thing if you're using RedCloth for
176 | # formatting in public places (e.g. Wikis) where you
177 | # don't want users to abuse HTML for bad things.
178 | #
179 | # If +:filter_html+ is set, HTML which wasn't
180 | # created by the Textile processor will be escaped.
181 | #
182 | # If +:filter_styles+ is set, it will also disable
183 | # the style markup specifier. ('{color: red}')
184 | #
185 | attr_accessor :filter_html, :filter_styles
186 |
187 | #
188 | # Accessor for toggling hard breaks.
189 | #
190 | # If +:hard_breaks+ is set, single newlines will
191 | # be converted to HTML break tags. This is the
192 | # default behavior for traditional RedCloth.
193 | #
194 | attr_accessor :hard_breaks
195 |
196 | # Accessor for toggling lite mode.
197 | #
198 | # In lite mode, block-level rules are ignored. This means
199 | # that tables, paragraphs, lists, and such aren't available.
200 | # Only the inline markup for bold, italics, entities and so on.
201 | #
202 | # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 | # r.to_html
204 | # #=> "And then? She fell!"
205 | #
206 | attr_accessor :lite_mode
207 |
208 | #
209 | # Accessor for toggling span caps.
210 | #
211 | # Textile places `span' tags around capitalized
212 | # words by default, but this wreaks havoc on Wikis.
213 | # If +:no_span_caps+ is set, this will be
214 | # suppressed.
215 | #
216 | attr_accessor :no_span_caps
217 |
218 | #
219 | # Establishes the markup predence. Available rules include:
220 | #
221 | # == Textile Rules
222 | #
223 | # The following textile rules can be set individually. Or add the complete
224 | # set of rules with the single :textile rule, which supplies the rule set in
225 | # the following precedence:
226 | #
227 | # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 | # block_textile_table:: Textile table block structures
229 | # block_textile_lists:: Textile list structures
230 | # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 | # inline_textile_image:: Textile inline images
232 | # inline_textile_link:: Textile inline links
233 | # inline_textile_span:: Textile inline spans
234 | # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 | #
236 | # == Markdown
237 | #
238 | # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 | # block_markdown_setext:: Markdown setext headers
240 | # block_markdown_atx:: Markdown atx headers
241 | # block_markdown_rule:: Markdown horizontal rules
242 | # block_markdown_bq:: Markdown blockquotes
243 | # block_markdown_lists:: Markdown lists
244 | # inline_markdown_link:: Markdown links
245 | attr_accessor :rules
246 |
247 | # Returns a new RedCloth object, based on _string_ and
248 | # enforcing all the included _restrictions_.
249 | #
250 | # r = RedCloth.new( "h1. A bold man", [:filter_html] )
251 | # r.to_html
252 | # #=>"A <b>bold</b> man
"
253 | #
254 | def initialize( string, restrictions = [] )
255 | restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 | super( string )
257 | end
258 |
259 | #
260 | # Generates HTML from the Textile contents.
261 | #
262 | # r = RedCloth.new( "And then? She *fell*!" )
263 | # r.to_html( true )
264 | # #=>"And then? She fell!"
265 | #
266 | def to_html( *rules )
267 | rules = DEFAULT_RULES if rules.empty?
268 | # make our working copy
269 | text = self.dup
270 |
271 | @urlrefs = {}
272 | @shelf = []
273 | textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
274 | :block_textile_prefix, :inline_textile_image, :inline_textile_link,
275 | :inline_textile_code, :inline_textile_span, :glyphs_textile]
276 | markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
277 | :block_markdown_bq, :block_markdown_lists,
278 | :inline_markdown_reflink, :inline_markdown_link]
279 | @rules = rules.collect do |rule|
280 | case rule
281 | when :markdown
282 | markdown_rules
283 | when :textile
284 | textile_rules
285 | else
286 | rule
287 | end
288 | end.flatten
289 |
290 | # standard clean up
291 | incoming_entities text
292 | clean_white_space text
293 |
294 | # start processor
295 | @pre_list = []
296 | rip_offtags text
297 | no_textile text
298 | hard_break text
299 | unless @lite_mode
300 | refs text
301 | blocks text
302 | end
303 | inline text
304 | smooth_offtags text
305 |
306 | retrieve text
307 |
308 | text.gsub!( /<\/?notextile>/, '' )
309 | text.gsub!( /x%x%/, '&' )
310 | clean_html text if filter_html
311 | text.strip!
312 | text
313 |
314 | end
315 |
316 | #######
317 | private
318 | #######
319 | #
320 | # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
321 | # (from PyTextile)
322 | #
323 | TEXTILE_TAGS =
324 |
325 | [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
326 | [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
327 | [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
328 | [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
329 | [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
330 |
331 | collect! do |a, b|
332 | [a.chr, ( b.zero? and "" or "{ b };" )]
333 | end
334 |
335 | #
336 | # Regular expressions to convert to HTML.
337 | #
338 | A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
339 | A_VLGN = /[\-^~]/
340 | C_CLAS = '(?:\([^)]+\))'
341 | C_LNGE = '(?:\[[^\]]+\])'
342 | C_STYL = '(?:\{[^}]+\})'
343 | S_CSPN = '(?:\\\\\d+)'
344 | S_RSPN = '(?:/\d+)'
345 | A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
346 | S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
347 | C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
348 | # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
349 | PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
350 | PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
351 | PUNCT_Q = Regexp::quote( '*-_+^~%' )
352 | HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
353 |
354 | # Text markup tags, don't conflict with block tags
355 | SIMPLE_HTML_TAGS = [
356 | 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
357 | 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
358 | 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
359 | ]
360 |
361 | QTAGS = [
362 | ['**', 'b'],
363 | ['*', 'strong'],
364 | ['??', 'cite', :limit],
365 | ['-', 'del', :limit],
366 | ['__', 'i'],
367 | ['_', 'em', :limit],
368 | ['%', 'span', :limit],
369 | ['+', 'ins', :limit],
370 | ['^', 'sup'],
371 | ['~', 'sub']
372 | ]
373 | QTAGS.collect! do |rc, ht, rtype|
374 | rcq = Regexp::quote rc
375 | re =
376 | case rtype
377 | when :limit
378 | /(\W)
379 | (#{rcq})
380 | (#{C})
381 | (?::(\S+?))?
382 | (\S.*?\S|\S)
383 | #{rcq}
384 | (?=\W)/x
385 | else
386 | /(#{rcq})
387 | (#{C})
388 | (?::(\S+))?
389 | (\S.*?\S|\S)
390 | #{rcq}/xm
391 | end
392 | [rc, ht, re, rtype]
393 | end
394 |
395 | # Elements to handle
396 | GLYPHS = [
397 | # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing
398 | [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1’' ], # single closing
399 | [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '’' ], # single closing
400 | [ /\'/, '‘' ], # single opening
401 | [ /, '<' ], # less-than
402 | [ />/, '>' ], # greater-than
403 | # [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing
404 | [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1”' ], # double closing
405 | [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '”' ], # double closing
406 | [ /"/, '“' ], # double opening
407 | [ /\b( )?\.{3}/, '\1…' ], # ellipsis
408 | [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '\1' ], # 3+ uppercase acronym
409 | [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^\2\3', :no_span_caps ], # 3+ uppercase caps
410 | [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash
411 | [ /\s->\s/, ' → ' ], # right arrow
412 | [ /\s-\s/, ' – ' ], # en dash
413 | [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign
414 | [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark
415 | [ /\b ?[(\[]R[\])]/i, '®' ], # registered
416 | [ /\b ?[(\[]C[\])]/i, '©' ] # copyright
417 | ]
418 |
419 | H_ALGN_VALS = {
420 | '<' => 'left',
421 | '=' => 'center',
422 | '>' => 'right',
423 | '<>' => 'justify'
424 | }
425 |
426 | V_ALGN_VALS = {
427 | '^' => 'top',
428 | '-' => 'middle',
429 | '~' => 'bottom'
430 | }
431 |
432 | #
433 | # Flexible HTML escaping
434 | #
435 | def htmlesc( str, mode )
436 | str.gsub!( '&', '&' )
437 | str.gsub!( '"', '"' ) if mode != :NoQuotes
438 | str.gsub!( "'", ''' ) if mode == :Quotes
439 | str.gsub!( '<', '<')
440 | str.gsub!( '>', '>')
441 | end
442 |
443 | # Search and replace for Textile glyphs (quotes, dashes, other symbols)
444 | def pgl( text )
445 | GLYPHS.each do |re, resub, tog|
446 | next if tog and method( tog ).call
447 | text.gsub! re, resub
448 | end
449 | end
450 |
451 | # Parses Textile attribute lists and builds an HTML attribute string
452 | def pba( text_in, element = "" )
453 |
454 | return '' unless text_in
455 |
456 | style = []
457 | text = text_in.dup
458 | if element == 'td'
459 | colspan = $1 if text =~ /\\(\d+)/
460 | rowspan = $1 if text =~ /\/(\d+)/
461 | style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
462 | end
463 |
464 | style << "#{ $1 };" if not filter_styles and
465 | text.sub!( /\{([^}]*)\}/, '' )
466 |
467 | lang = $1 if
468 | text.sub!( /\[([^)]+?)\]/, '' )
469 |
470 | cls = $1 if
471 | text.sub!( /\(([^()]+?)\)/, '' )
472 |
473 | style << "padding-left:#{ $1.length }em;" if
474 | text.sub!( /([(]+)/, '' )
475 |
476 | style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
477 |
478 | style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
479 |
480 | cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
481 |
482 | atts = ''
483 | atts << " style=\"#{ style.join }\"" unless style.empty?
484 | atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
485 | atts << " lang=\"#{ lang }\"" if lang
486 | atts << " id=\"#{ id }\"" if id
487 | atts << " colspan=\"#{ colspan }\"" if colspan
488 | atts << " rowspan=\"#{ rowspan }\"" if rowspan
489 |
490 | atts
491 | end
492 |
493 | TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
494 |
495 | # Parses a Textile table block, building HTML from the result.
496 | def block_textile_table( text )
497 | text.gsub!( TABLE_RE ) do |matches|
498 |
499 | tatts, fullrow = $~[1..2]
500 | tatts = pba( tatts, 'table' )
501 | tatts = shelve( tatts ) if tatts
502 | rows = []
503 |
504 | fullrow.
505 | split( /\|$/m ).
506 | delete_if { |x| x.empty? }.
507 | each do |row|
508 |
509 | ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
510 |
511 | cells = []
512 | row.split( '|' ).each do |cell|
513 | ctyp = 'd'
514 | ctyp = 'h' if cell =~ /^_/
515 |
516 | catts = ''
517 | catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
518 |
519 | unless cell.strip.empty?
520 | catts = shelve( catts ) if catts
521 | cells << "\t\t\t#{ cell }"
522 | end
523 | end
524 | ratts = shelve( ratts ) if ratts
525 | rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t
"
526 | end
527 | "\t\n#{ rows.join( "\n" ) }\n\t
\n\n"
528 | end
529 | end
530 |
531 | LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
532 | LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
533 |
534 | # Parses Textile lists and generates HTML
535 | def block_textile_lists( text )
536 | text.gsub!( LISTS_RE ) do |match|
537 | lines = match.split( /\n/ )
538 | last_line = -1
539 | depth = []
540 | lines.each_with_index do |line, line_id|
541 | if line =~ LISTS_CONTENT_RE
542 | tl,atts,content = $~[1..3]
543 | if depth.last
544 | if depth.last.length > tl.length
545 | (depth.length - 1).downto(0) do |i|
546 | break if depth[i].length == tl.length
547 | lines[line_id - 1] << "\n\t#{ lT( depth[i] ) }l>\n\t"
548 | depth.pop
549 | end
550 | end
551 | if depth.last and depth.last.length == tl.length
552 | lines[line_id - 1] << ''
553 | end
554 | end
555 | unless depth.last == tl
556 | depth << tl
557 | atts = pba( atts )
558 | atts = shelve( atts ) if atts
559 | lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t#{ content }"
560 | else
561 | lines[line_id] = "\t\t#{ content }"
562 | end
563 | last_line = line_id
564 |
565 | else
566 | last_line = line_id
567 | end
568 | if line_id - last_line > 1 or line_id == lines.length - 1
569 | depth.delete_if do |v|
570 | lines[last_line] << "\n\t#{ lT( v ) }l>"
571 | end
572 | end
573 | end
574 | lines.join( "\n" )
575 | end
576 | end
577 |
578 | CODE_RE = /(\W)
579 | @
580 | (?:\|(\w+?)\|)?
581 | (.+?)
582 | @
583 | (?=\W)/x
584 |
585 | def inline_textile_code( text )
586 | text.gsub!( CODE_RE ) do |m|
587 | before,lang,code,after = $~[1..4]
588 | lang = " lang=\"#{ lang }\"" if lang
589 | rip_offtags( "#{ before }#{ code }#{ after }" )
590 | end
591 | end
592 |
593 | def lT( text )
594 | text =~ /\#$/ ? 'o' : 'u'
595 | end
596 |
597 | def hard_break( text )
598 | text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
" ) if hard_breaks
599 | end
600 |
601 | BLOCKS_GROUP_RE = /\n{2,}(?! )/m
602 |
603 | def blocks( text, deep_code = false )
604 | text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
605 | plain = blk !~ /\A[#*> ]/
606 |
607 | # skip blocks that are complex HTML
608 | if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
609 | blk
610 | else
611 | # search for indentation levels
612 | blk.strip!
613 | if blk.empty?
614 | blk
615 | else
616 | code_blk = nil
617 | blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
618 | flush_left iblk
619 | blocks iblk, plain
620 | iblk.gsub( /^(\S)/, "\t\\1" )
621 | if plain
622 | code_blk = iblk; ""
623 | else
624 | iblk
625 | end
626 | end
627 |
628 | block_applied = 0
629 | @rules.each do |rule_name|
630 | block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
631 | end
632 | if block_applied.zero?
633 | if deep_code
634 | blk = "\t#{ blk }
"
635 | else
636 | blk = "\t#{ blk }
"
637 | end
638 | end
639 | # hard_break blk
640 | blk + "\n#{ code_blk }"
641 | end
642 | end
643 |
644 | end.join( "\n\n" ) )
645 | end
646 |
647 | def textile_bq( tag, atts, cite, content )
648 | cite, cite_title = check_refs( cite )
649 | cite = " cite=\"#{ cite }\"" if cite
650 | atts = shelve( atts ) if atts
651 | "\t\n\t\t#{ content }
\n\t
"
652 | end
653 |
654 | def textile_p( tag, atts, cite, content )
655 | atts = shelve( atts ) if atts
656 | "\t<#{ tag }#{ atts }>#{ content }#{ tag }>"
657 | end
658 |
659 | alias textile_h1 textile_p
660 | alias textile_h2 textile_p
661 | alias textile_h3 textile_p
662 | alias textile_h4 textile_p
663 | alias textile_h5 textile_p
664 | alias textile_h6 textile_p
665 |
666 | def textile_fn_( tag, num, atts, cite, content )
667 | atts << " id=\"fn#{ num }\""
668 | content = "#{ num } #{ content }"
669 | atts = shelve( atts ) if atts
670 | "\t#{ content }
"
671 | end
672 |
673 | BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
674 |
675 | def block_textile_prefix( text )
676 | if text =~ BLOCK_RE
677 | tag,tagpre,num,atts,cite,content = $~[1..6]
678 | atts = pba( atts )
679 |
680 | # pass to prefix handler
681 | if respond_to? "textile_#{ tag }", true
682 | text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
683 | elsif respond_to? "textile_#{ tagpre }_", true
684 | text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
685 | end
686 | end
687 | end
688 |
689 | SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
690 | def block_markdown_setext( text )
691 | if text =~ SETEXT_RE
692 | tag = if $2 == "="; "h1"; else; "h2"; end
693 | blk, cont = "<#{ tag }>#{ $1 }#{ tag }>", $'
694 | blocks cont
695 | text.replace( blk + cont )
696 | end
697 | end
698 |
699 | ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
700 | [ ]*
701 | (.+?) # $2 = Header text
702 | [ ]*
703 | \#* # optional closing #'s (not counted)
704 | $/x
705 | def block_markdown_atx( text )
706 | if text =~ ATX_RE
707 | tag = "h#{ $1.length }"
708 | blk, cont = "<#{ tag }>#{ $2 }#{ tag }>\n\n", $'
709 | blocks cont
710 | text.replace( blk + cont )
711 | end
712 | end
713 |
714 | MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
715 |
716 | def block_markdown_bq( text )
717 | text.gsub!( MARKDOWN_BQ_RE ) do |blk|
718 | blk.gsub!( /^ *> ?/, '' )
719 | flush_left blk
720 | blocks blk
721 | blk.gsub!( /^(\S)/, "\t\\1" )
722 | "\n#{ blk }\n
\n\n"
723 | end
724 | end
725 |
726 | MARKDOWN_RULE_RE = /^(#{
727 | ['*', '-', '_'].collect { |ch| '( ?' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
728 | })$/
729 |
730 | def block_markdown_rule( text )
731 | text.gsub!( MARKDOWN_RULE_RE ) do |blk|
732 | "
"
733 | end
734 | end
735 |
736 | # XXX TODO XXX
737 | def block_markdown_lists( text )
738 | end
739 |
740 | def inline_textile_span( text )
741 | QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
742 | text.gsub!( qtag_re ) do |m|
743 |
744 | case rtype
745 | when :limit
746 | sta,qtag,atts,cite,content = $~[1..5]
747 | else
748 | qtag,atts,cite,content = $~[1..4]
749 | sta = ''
750 | end
751 | atts = pba( atts )
752 | atts << " cite=\"#{ cite }\"" if cite
753 | atts = shelve( atts ) if atts
754 |
755 | "#{ sta }<#{ ht }#{ atts }>#{ content }#{ ht }>"
756 |
757 | end
758 | end
759 | end
760 |
761 | LINK_RE = /
762 | ([\s\[{(]|[#{PUNCT}])? # $pre
763 | " # start
764 | (#{C}) # $atts
765 | ([^"]+?) # $text
766 | \s?
767 | (?:\(([^)]+?)\)(?="))? # $title
768 | ":
769 | (\S+?) # $url
770 | (\/)? # $slash
771 | ([^\w\/;]*?) # $post
772 | (?=<|\s|$)
773 | /x
774 |
775 | def inline_textile_link( text )
776 | text.gsub!( LINK_RE ) do |m|
777 | pre,atts,text,title,url,slash,post = $~[1..7]
778 |
779 | url, url_title = check_refs( url )
780 | title ||= url_title
781 |
782 | atts = pba( atts )
783 | atts = " href=\"#{ url }#{ slash }\"#{ atts }"
784 | atts << " title=\"#{ title }\"" if title
785 | atts = shelve( atts ) if atts
786 |
787 | "#{ pre }#{ text }#{ post }"
788 | end
789 | end
790 |
791 | MARKDOWN_REFLINK_RE = /
792 | \[([^\[\]]+)\] # $text
793 | [ ]? # opt. space
794 | (?:\n[ ]*)? # one optional newline followed by spaces
795 | \[(.*?)\] # $id
796 | /x
797 |
798 | def inline_markdown_reflink( text )
799 | text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
800 | text, id = $~[1..2]
801 |
802 | if id.empty?
803 | url, title = check_refs( text )
804 | else
805 | url, title = check_refs( id )
806 | end
807 |
808 | atts = " href=\"#{ url }\""
809 | atts << " title=\"#{ title }\"" if title
810 | atts = shelve( atts )
811 |
812 | "#{ text }"
813 | end
814 | end
815 |
816 | MARKDOWN_LINK_RE = /
817 | \[([^\[\]]+)\] # $text
818 | \( # open paren
819 | [ \t]* # opt space
820 | (.+?)>? # $href
821 | [ \t]* # opt space
822 | (?: # whole title
823 | (['"]) # $quote
824 | (.*?) # $title
825 | \3 # matching quote
826 | )? # title is optional
827 | \)
828 | /x
829 |
830 | def inline_markdown_link( text )
831 | text.gsub!( MARKDOWN_LINK_RE ) do |m|
832 | text, url, quote, title = $~[1..4]
833 |
834 | atts = " href=\"#{ url }\""
835 | atts << " title=\"#{ title }\"" if title
836 | atts = shelve( atts )
837 |
838 | "#{ text }"
839 | end
840 | end
841 |
842 | TEXTILE_REFS_RE = /(^ *)\[([^\n]+?)\](#{HYPERLINK})(?=\s|$)/
843 | MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
844 |
845 | def refs( text )
846 | @rules.each do |rule_name|
847 | method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
848 | end
849 | end
850 |
851 | def refs_textile( text )
852 | text.gsub!( TEXTILE_REFS_RE ) do |m|
853 | flag, url = $~[2..3]
854 | @urlrefs[flag.downcase] = [url, nil]
855 | nil
856 | end
857 | end
858 |
859 | def refs_markdown( text )
860 | text.gsub!( MARKDOWN_REFS_RE ) do |m|
861 | flag, url = $~[2..3]
862 | title = $~[6]
863 | @urlrefs[flag.downcase] = [url, title]
864 | nil
865 | end
866 | end
867 |
868 | def check_refs( text )
869 | ret = @urlrefs[text.downcase] if text
870 | ret || [text, nil]
871 | end
872 |
873 | IMAGE_RE = /
874 | (|.|^) # start of line?
875 | \! # opening
876 | (\<|\=|\>)? # optional alignment atts
877 | (#{C}) # optional style,class atts
878 | (?:\. )? # optional dot-space
879 | ([^\s(!]+?) # presume this is the src
880 | \s? # optional space
881 | (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
882 | \! # closing
883 | (?::#{ HYPERLINK })? # optional href
884 | /x
885 |
886 | def inline_textile_image( text )
887 | text.gsub!( IMAGE_RE ) do |m|
888 | stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
889 | atts = pba( atts )
890 | atts = " src=\"#{ url }\"#{ atts }"
891 | atts << " title=\"#{ title }\"" if title
892 | atts << " alt=\"#{ title }\""
893 | # size = @getimagesize($url);
894 | # if($size) $atts.= " $size[3]";
895 |
896 | href, alt_title = check_refs( href ) if href
897 | url, url_title = check_refs( url )
898 |
899 | out = ''
900 | out << "" if href
901 | out << "
"
902 | out << "#{ href_a1 }#{ href_a2 }" if href
903 |
904 | if algn
905 | algn = h_align( algn )
906 | if stln == "
"
907 | out = "
#{ out }"
908 | else
909 | out = "#{ stln }
#{ out }
"
910 | end
911 | else
912 | out = stln + out
913 | end
914 |
915 | out
916 | end
917 | end
918 |
919 | def shelve( val )
920 | @shelf << val
921 | " :redsh##{ @shelf.length }:"
922 | end
923 |
924 | def retrieve( text )
925 | @shelf.each_with_index do |r, i|
926 | text.gsub!( " :redsh##{ i + 1 }:", r )
927 | end
928 | end
929 |
930 | def incoming_entities( text )
931 | ## turn any incoming ampersands into a dummy character for now.
932 | ## This uses a negative lookahead for alphanumerics followed by a semicolon,
933 | ## implying an incoming html entity, to be skipped
934 |
935 | text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
936 | end
937 |
938 | def no_textile( text )
939 | text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
940 | '\1\2\3' )
941 | text.gsub!( /^ *==([^=]+.*?)==/m,
942 | '\1\2\3' )
943 | end
944 |
945 | def clean_white_space( text )
946 | # normalize line breaks
947 | text.gsub!( /\r\n/, "\n" )
948 | text.gsub!( /\r/, "\n" )
949 | text.gsub!( /\t/, ' ' )
950 | text.gsub!( /^ +$/, '' )
951 | text.gsub!( /\n{3,}/, "\n\n" )
952 | text.gsub!( /"$/, "\" " )
953 |
954 | # if entire document is indented, flush
955 | # to the left side
956 | flush_left text
957 | end
958 |
959 | def flush_left( text )
960 | indt = 0
961 | if text =~ /^ /
962 | while text !~ /^ {#{indt}}\S/
963 | indt += 1
964 | end unless text.empty?
965 | if indt.nonzero?
966 | text.gsub!( /^ {#{indt}}/, '' )
967 | end
968 | end
969 | end
970 |
971 | def footnote_ref( text )
972 | text.gsub!( /\b\[([0-9]+?)\](\s)?/,
973 | '\1\2' )
974 | end
975 |
976 | OFFTAGS = /(code|pre|kbd|notextile)/
977 | OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi
978 | OFFTAG_OPEN = /<#{ OFFTAGS }/
979 | OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
980 | HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
981 | ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
982 |
983 | def glyphs_textile( text, level = 0 )
984 | if text !~ HASTAG_MATCH
985 | pgl text
986 | footnote_ref text
987 | else
988 | codepre = 0
989 | text.gsub!( ALLTAG_MATCH ) do |line|
990 | ## matches are off if we're between , etc.
991 | if $1
992 | if line =~ OFFTAG_OPEN
993 | codepre += 1
994 | elsif line =~ OFFTAG_CLOSE
995 | codepre -= 1
996 | codepre = 0 if codepre < 0
997 | end
998 | elsif codepre.zero?
999 | glyphs_textile( line, level + 1 )
1000 | else
1001 | htmlesc( line, :NoQuotes )
1002 | end
1003 | # p [level, codepre, line]
1004 |
1005 | line
1006 | end
1007 | end
1008 | end
1009 |
1010 | def rip_offtags( text )
1011 | if text =~ /<.*>/
1012 | ## strip and encode content
1013 | codepre, used_offtags = 0, {}
1014 | text.gsub!( OFFTAG_MATCH ) do |line|
1015 | if $3
1016 | offtag, aftertag = $4, $5
1017 | codepre += 1
1018 | used_offtags[offtag] = true
1019 | if codepre - used_offtags.length > 0
1020 | htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
1021 | @pre_list.last << line
1022 | line = ""
1023 | else
1024 | htmlesc( aftertag, :NoQuotes ) if aftertag and not used_offtags['notextile']
1025 | line = ""
1026 | @pre_list << "#{ $3 }#{ aftertag }"
1027 | end
1028 | elsif $1 and codepre > 0
1029 | if codepre - used_offtags.length > 0
1030 | htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
1031 | @pre_list.last << line
1032 | line = ""
1033 | end
1034 | codepre -= 1 unless codepre.zero?
1035 | used_offtags = {} if codepre.zero?
1036 | end
1037 | line
1038 | end
1039 | end
1040 | text
1041 | end
1042 |
1043 | def smooth_offtags( text )
1044 | unless @pre_list.empty?
1045 | ## replace content
1046 | text.gsub!( // ) { @pre_list[$1.to_i] }
1047 | end
1048 | end
1049 |
1050 | def inline( text )
1051 | [/^inline_/, /^glyphs_/].each do |meth_re|
1052 | @rules.each do |rule_name|
1053 | method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1054 | end
1055 | end
1056 | end
1057 |
1058 | def h_align( text )
1059 | H_ALGN_VALS[text]
1060 | end
1061 |
1062 | def v_align( text )
1063 | V_ALGN_VALS[text]
1064 | end
1065 |
1066 | def textile_popup_help( name, windowW, windowH )
1067 | ' ' + name + '
'
1068 | end
1069 |
1070 | # HTML cleansing stuff
1071 | BASIC_TAGS = {
1072 | 'a' => ['href', 'title'],
1073 | 'img' => ['src', 'alt', 'title'],
1074 | 'br' => [],
1075 | 'i' => nil,
1076 | 'u' => nil,
1077 | 'b' => nil,
1078 | 'pre' => nil,
1079 | 'kbd' => nil,
1080 | 'code' => ['lang'],
1081 | 'cite' => nil,
1082 | 'strong' => nil,
1083 | 'em' => nil,
1084 | 'ins' => nil,
1085 | 'sup' => nil,
1086 | 'sub' => nil,
1087 | 'del' => nil,
1088 | 'table' => nil,
1089 | 'tr' => nil,
1090 | 'td' => ['colspan', 'rowspan'],
1091 | 'th' => nil,
1092 | 'ol' => nil,
1093 | 'ul' => nil,
1094 | 'li' => nil,
1095 | 'p' => nil,
1096 | 'h1' => nil,
1097 | 'h2' => nil,
1098 | 'h3' => nil,
1099 | 'h4' => nil,
1100 | 'h5' => nil,
1101 | 'h6' => nil,
1102 | 'blockquote' => ['cite']
1103 | }
1104 |
1105 | def clean_html( text, tags = BASIC_TAGS )
1106 | text.gsub!( /]*)>/ ) do
1108 | raw = $~
1109 | tag = raw[2].downcase
1110 | if tags.has_key? tag
1111 | pcs = [tag]
1112 | tags[tag].each do |prop|
1113 | ['"', "'", ''].each do |q|
1114 | q2 = ( q != '' ? q : '\s' )
1115 | if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1116 | attrv = $1
1117 | next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1118 | pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1119 | break
1120 | end
1121 | end
1122 | end if tags[tag]
1123 | "<#{raw[1]}#{pcs.join " "}>"
1124 | else
1125 | " "
1126 | end
1127 | end
1128 | end
1129 | end
1130 |
1131 |
--------------------------------------------------------------------------------