Created
August 18, 2011 04:04
-
-
Save sublimator/1153273 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
#coding: utf8 | |
#################################### IMPORTS ################################### | |
# Std Libs | |
import cgi | |
import os | |
import pprint | |
import string | |
import time | |
import webbrowser | |
from os.path import split, join, normpath, splitext | |
# Sublime Modules | |
import sublime | |
import sublime_plugin | |
# APP Libs | |
from plist import parse_plist | |
################################# SETTINGS ##################################### | |
settings = sublime.load_settings('html-export.sublime-settings') | |
defaults = { | |
"open_html_in_editor" : False, | |
"open_html_in_browser" : True, | |
"copy_css_to_clipboard" : True, | |
"copy_html_to_clipboard" : True, | |
"encode_as" : "utf-8", | |
"add_line_numbers" : True | |
} | |
class Settings(object): | |
def __init__(self, kw): | |
self.kw = kw | |
def __getattr__(self, attr): | |
if attr in getattr(self, 'kw'): return self.kw.get(attr) | |
if settings.has(attr): return settings.get(attr) | |
else: return defaults.get(attr) | |
################################### TODO ####################################### | |
""" | |
clean up 4 000,000 line function | |
multiple selections, for posting snippets with foo() ... bar() = DONE = | |
contract selections so there are no empty lines 4 mult-sel | |
pre tag + css to clipboard = almost DONE = | |
compress clipboard css = DONE = | |
embedded style="bla" attributes, no messing around with style | |
sheets for quick posts | |
in <head> css styles | |
line numbers in float:left pre tag so can easily copy paste | |
""" | |
################################# TEMPLATES #################################### | |
HTML_TEMPLATE = """<html> | |
<head> | |
<title> %s </title> | |
<link rel="stylesheet" href="%s.css" type="text/css" charset="utf-8" /> | |
<style type='text/css'> | |
body {margin:0; padding:0;} | |
pre {padding: 1em; font: 12px "DejaVu Sans Mono", monospace;} | |
</style> | |
</head> | |
<body> | |
%s | |
</body> | |
</html> | |
""" | |
LINE_NUMBER = '<span class="lineNumber">%s</span>' | |
################################### BUILD CSS ################################## | |
CSSMAP = { | |
'background': 'background-color', | |
'caret': None, | |
'fontStyle': None, | |
'foreground': 'color', | |
'invisibles': None, | |
'lineHighlight': None, | |
'selection': None, | |
} | |
################################################################################ | |
def camelize_string(to_camel): | |
to_camel = [(l if l in string.ascii_letters else ' ') for l in to_camel] | |
to_camel = [w.capitalize() for w in "".join(to_camel).split(' ')] | |
to_camel = "".join(to_camel) | |
return to_camel[:1].lower() + to_camel[1:] #sometimes to_camel will be '' | |
def create_title(input_dict): | |
name, author = input_dict['name'], input_dict['author'] | |
return "Theme: %s\nAuthor: %s" % (name, author) | |
def get_css_from_tm_settings(settings): | |
css = {} | |
for rule in settings: | |
if rule in CSSMAP and CSSMAP[rule]: | |
css[CSSMAP[rule]] = settings[rule] | |
return sorted(css.items()) | |
def create_rule(rule_starter, listing): | |
rule = rule_starter | |
css = get_css_from_tm_settings(listing['settings']) | |
for property_, value in css: | |
if property_ in ("background-color", 'color'): | |
value = value[:7] | |
rule.append(" %s: %s;" % (property_, value)) | |
rule.append("}") | |
return "\n".join(rule) | |
def create_main_rule(listing, theme_name): | |
return create_rule(["pre.%s {" % camelize_string(theme_name)], listing) | |
def create_scope_rule(listing, theme_name): | |
name = camelize_string(listing['name']) | |
return create_rule(["pre.%s .%s {" % (camelize_string(theme_name), name)], | |
listing) | |
def get_css_from_theme_dict(theme): | |
name = theme['name'] | |
settings = theme['settings'] | |
main_settings = settings[0] | |
css = [create_main_rule(main_settings, name)] | |
for scope_rule in settings[1:]: | |
css.append(create_scope_rule(scope_rule, name)) | |
css+=["pre.%s .lineNumber {\n color: #7f909f;\n}" % camelize_string(name)] | |
return "\n\n".join(css) | |
def get_scopes(theme): | |
scopes = {} | |
for scope in theme["settings"][1:]: | |
if 'scope' in scope: | |
scopes[scope['scope']] = camelize_string(scope['name']) | |
return tuple(scopes.items()) | |
#################################### HELPERS ################################### | |
def memoize(func): | |
"Implementation taken from python test suite" | |
saved = {} | |
def call(*args): | |
try: | |
return saved[args] | |
except KeyError: | |
res = func(*args) | |
saved[args] = res | |
return res | |
except TypeError: | |
# Unhashable argument | |
return func(*args) | |
call.func_name = func.func_name | |
return call | |
def write_html(html, fn, theme): | |
with open('%s.html' % fn, 'w') as fh: | |
html = HTML_TEMPLATE % (fn, camelize_string(theme), html) | |
fh.write(html.encode(settings.get('encode_as'))) | |
return '%s.html' % fn #TODO: refactor | |
def get_theme_name(color_scheme): | |
return splitext(split(color_scheme)[1])[0] | |
def get_theme_abs_path(color_scheme): | |
appdata_path = split(sublime.packages_path())[0] | |
return normpath(join(appdata_path, color_scheme)) | |
@memoize | |
def get_css_rules(color_scheme): | |
rules = get_scopes(parse_plist(get_theme_abs_path(color_scheme))) | |
sublime.set_clipboard(pprint.pformat(rules)) | |
return rules | |
def get_view_css(view): | |
color_scheme = view.settings().get('color_scheme') | |
# theme = get_theme_name(color_scheme) | |
return get_css_rules(color_scheme) | |
def write_css(color_scheme, wd=None): | |
theme = camelize_string(get_theme_name(color_scheme)) | |
theme_p_list = get_theme_abs_path(color_scheme) | |
css = get_css_from_theme_dict(parse_plist(theme_p_list)) | |
with open(os.path.join(wd, "%s.css" % theme), 'w') as fh: | |
fh.write(css.encode(settings.get('encode_as'))) | |
return css #TODO fix this | |
def selections_or_full_buffer_if_empty(view): | |
if view.has_non_empty_selection_region(): | |
return [view.line(s) for s in view.sel()] | |
else: | |
return [sublime.Region(0, view.size())] | |
################################# COMMANDS ##################################### | |
def scope_events(view, rng=None): | |
rng = rng or xrange(0, view.size()) | |
opened = [] | |
for pt in rng: | |
scope = view.scope_name(pt).rstrip() | |
ch = view.substr(pt) | |
for i, (os, ss) in enumerate(map(None, opened, scope.split())): | |
if not ss or os != ss: | |
for ev in opened[i:]: | |
yield (pt, 'close', os) | |
del opened[i:] | |
if ss and (not os or os != ss): | |
opened.append(ss) | |
yield (pt, 'open', ' '.join(opened)) | |
yield (pt, 'char', ch) | |
# close up shop | |
for p in opened: | |
yield (pt, 'close', None) | |
def sort_by_selector ( scope, | |
to_sort, | |
selector_index=0, | |
keep_non_matches=True ): | |
candidates =[] | |
for i, item in enumerate(to_sort): | |
selector = item[selector_index] | |
specificity = sublime.score_selector(scope, selector ) | |
if specificity or keep_non_matches: | |
candidates.append((specificity, i )) | |
return [to_sort[i[1]] for i in sorted( candidates, | |
key=lambda c: c[0], reverse=True)] | |
@memoize | |
def css_class_for_scope(scope, color_scheme): | |
css_rules = get_css_rules(color_scheme) | |
candidates = sort_by_selector( scope, css_rules, keep_non_matches=False) | |
if candidates: | |
return candidates[0][1] | |
class HtmlExportCommand(sublime_plugin.TextCommand): | |
def run(self, edit, **kw): | |
view = self.view | |
t = time.time() | |
s = Settings(kw) | |
# Perf boost? | |
add_line_numbers = s.add_line_numbers | |
copy_css_to_clipboard = s.copy_css_to_clipboard | |
copy_html_to_clipboard = s.copy_html_to_clipboard | |
open_html_in_browser = s.open_html_in_browser | |
open_html_in_editor = s.open_html_in_editor | |
color_scheme = view.settings().get('color_scheme') | |
theme = get_theme_name(color_scheme) | |
html = ["<pre class='%s'>" % camelize_string(theme)] | |
selections = selections_or_full_buffer_if_empty(view) | |
ln_cols = len(`view.rowcol(selections[-1].end()-1)[0]`) | |
for i, sel in enumerate(selections): | |
if i > 0: html += [LINE_NUMBER % ("\n\n%s\n\n" % (ln_cols * '.'))] | |
if add_line_numbers: | |
current_line_number = view.rowcol(sel.begin())[0] + 1 | |
line_numbers_template = LINE_NUMBER % ("%"+ `ln_cols` + "d ") | |
html += [line_numbers_template % current_line_number] | |
for pt, event, value in scope_events(view, xrange(sel.begin(), sel.end())): | |
if event == 'open': | |
kls = css_class_for_scope(value, color_scheme) | |
html.append("<span class='%s'>" % kls) | |
elif event == 'close': | |
html.append("</span>") | |
else: # event == 'char' | |
html.append(cgi.escape(value)) | |
if add_line_numbers and value == '\n': | |
current_line_number += 1 | |
html += [line_numbers_template % current_line_number] | |
html = "".join(html + ["</pre>"]) | |
sublime.status_message ( | |
"HTML and CSS conversion complete: %s " % (time.time() -t) ) | |
write_out_html = open_html_in_editor or open_html_in_browser | |
if write_out_html: | |
html_file = write_html(html, view.file_name(), theme) # HACK TODO | |
css_string = write_css(color_scheme, wd=os.path.dirname( | |
str(view.file_name()))) | |
if open_html_in_browser: webbrowser.open(html_file) | |
if open_html_in_editor: view.window().open_file(html_file) | |
clipboard = '' | |
if copy_css_to_clipboard: | |
# compress the css | |
css = ' '.join([l.strip('\n') for l in css_string.split('\n')]) | |
clipboard += ' '.join([w.strip(' ') for w in css.split(' ')]) + '\n' | |
if copy_html_to_clipboard: clipboard += html | |
if clipboard: | |
sublime.set_clipboard(clipboard) | |
################################################################################ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment