├── 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 "![${1:#{alt}}](#{url})" 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{${1:#{CGI::escapeHTML alt}}} 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 | # Textist 108 | # 109 | # With a link: 110 | # 111 | # !/common/textist.gif(Textist)!:http://textism.com 112 | # 113 | # Will become: 114 | # 115 | # Textist 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 | [ //, '>' ], # 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\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" 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 }" 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 }", $' 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 }\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 }" 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+?(?:\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 | --------------------------------------------------------------------------------