From d59bc012b6dc0e16108314e43fb2a69d32e5c175 Mon Sep 17 00:00:00 2001 From: Aparajita Fishman Date: Thu, 5 Apr 2012 20:25:14 -0700 Subject: [PATCH] Added dump_theme tool --- Tools/Jakefile | 2 +- Tools/dump_theme/Jakefile | 11 + Tools/dump_theme/dump_theme | 429 ++++++++++++++++++++++++++++++++++ Tools/dump_theme/dump_theme.1 | 48 ++++ 4 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 Tools/dump_theme/Jakefile create mode 100755 Tools/dump_theme/dump_theme create mode 100644 Tools/dump_theme/dump_theme.1 diff --git a/Tools/Jakefile b/Tools/Jakefile index 803dc2a7a..a32dfbd3b 100644 --- a/Tools/Jakefile +++ b/Tools/Jakefile @@ -2,6 +2,6 @@ require("../common.jake"); // nib2cib uses fontinfo and capp uses nib2cib -subtasks(["fontinfo", "nib2cib", "capp", "NativeHost", "XcodeCapp"], ["build"/*, "clean", "clobber"*/]); +subtasks(["fontinfo", "nib2cib", "capp", "dump_theme", "NativeHost", "XcodeCapp"], ["build"/*, "clean", "clobber"*/]); subtasks(["fontinfo"], ["clean", "clobber"]); diff --git a/Tools/dump_theme/Jakefile b/Tools/dump_theme/Jakefile new file mode 100644 index 000000000..eac854343 --- /dev/null +++ b/Tools/dump_theme/Jakefile @@ -0,0 +1,11 @@ + +require ("../../common.jake"); + +var FILE = require("file"), + SYSTEM = require("system"), + path = FILE.Path(FILE.join(SYSTEM.prefix, "bin", "dump_theme")); + +FILE.copy("dump_theme", path); +path.chmod(0755); + +copyManPage("dump_theme", 1); diff --git a/Tools/dump_theme/dump_theme b/Tools/dump_theme/dump_theme new file mode 100755 index 000000000..d4d9efe16 --- /dev/null +++ b/Tools/dump_theme/dump_theme @@ -0,0 +1,429 @@ +#!/usr/bin/env objj + +@import +@import +@import + +var FILE = require("file"), + OS = require("os"), + + DefaultTheme = "Aristo", + BuildTypes = ["Debug", "Release"], + + FILE = require("file"), + OS = require("os"), + SYS = require("system"), + FileList = require("jake").FileList, + stream = require("narwhal/term").stream, + Parser = require("narwhal/args").Parser, + options = null, + + ImageDescriptionFormat = "<%s> {\n filename: \"%s\",\n size: { width:%f, height:%f }\n}", + + DumpThemeException = @"DumpThemeException"; + + +function main(args) +{ + try + { + var dumper = [ThemeDumper new]; + [dumper dumpWithArgs:args]; + } + catch (e) + { + if ([e isKindOfClass:CPException]) + colorPrint("red", [e reason]); + + OS.exit(1); + } +} + +@implementation ThemeDumper : CPObject +{ + JSObject parser; + int mark; +} + +- (id)init +{ + if (self = [super init]) + { + parser = new Parser(); + + // Skip the static data header + mark = @"@STATIC;".length; + } + + return self; +} + +- (void)dumpWithArgs:(CPArray)args +{ + [self parseArgs:args]; + + var themeName = options.args[0] || "", + themeDir = ""; + + if (themeName && (themeName.lastIndexOf(".blend") === (themeName.length - ".blend".length))) + { + themeDir = FILE.canonical(FILE.dirname(themeName)); + themeName = FILE.basename(themeName, FILE.extension(themeName)); + } + + themeName = themeName || [self getAppKitDefaultThemeName]; + + if (!themeName) + [CPException raise:DumpThemeException reason:@"Could not determine the theme name."]; + + colorPrint("purple", "Theme name: " + themeName); + + var theme = [self loadTheme:themeName fromDirectory:themeDir]; + + if (!theme) + [CPException raise:DumpThemeException reason:@"Could not load the theme \"" + themeName + @"\""]; + + [self dump:theme]; +} + +- (void)parseArgs:(CPArray)args +{ + parser.usage("[--no-color] [NAME_OR_BLEND_PATH]"); + + parser.option("--no-color", "colorize") + .set(false) + .def(true) + .help("Colorize the output. Should only be used when dumping to the terminal."); + + parser.helpful(); + + options = parser.parse(args, null, null, true); + + if (options.args.length > 1) + { + parser.printUsage(options); + OS.exit(0); + } +} + +- (void)dump:(CPTheme)theme +{ + var classNames = [theme classNames]; + + classNames.sort(); + + for (var i = 0; i < classNames.length; ++i) + { + var className = classNames[i], + attributes = [theme attributeNamesForClass:className]; + + colorPrint("green", "---------------------------\n" + className + "\n---------------------------"); + attributes.sort(); + + for (var attributeIndex = 0; attributeIndex < attributes.length; ++attributeIndex) + { + var attributeName = attributes[attributeIndex], + values = [[theme attributeWithName:attributeName forClass:className] values], + states = [values allKeys]; + + colorPrint("violet", attributeName); + states.sort(); + + for (var stateIndex = 0; stateIndex < states.length; ++stateIndex) + { + var state = states[stateIndex], + value = [theme valueForAttributeWithName:attributeName inState:state forClass:className], + description = [self descriptionForValue:value].replace(/^/mg, " "); + + colorPrint("cyan", " " + CPThemeStateName(state)); + stream.print(description + "\n"); + } + } + + stream.print(""); + } +} + +- (CPString)descriptionForValue:(id)aValue +{ + var description = @""; + + if (!aValue) + return @""; + + if (!aValue.isa) + { + if (typeof(aValue) === "object") + { + if (aValue.hasOwnProperty("width") && aValue.hasOwnProperty("height")) + description = [CPString stringWithFormat:@"CGSize: { width:%f, height:%f }", aValue.width, aValue.height]; + else if (aValue.hasOwnProperty("x") && aValue.hasOwnProperty("y")) + description = [CPString stringWithFormat:@"CGPoint: { x:%f, y:%f }", aValue.x, aValue.y]; + else if (aValue.hasOwnProperty("origin") && aValue.hasOwnProperty("size")) + description = [CPString stringWithFormat:@"CGRect: { x:%f, y:%f }, { width:%f, height:%f }", aValue.origin.x, aValue.origin.y, aValue.size.width, aValue.size.height]; + else if (aValue.hasOwnProperty("top") && aValue.hasOwnProperty("right") && aValue.hasOwnProperty("bottom") && aValue.hasOwnProperty("left")) + description = [CPString stringWithFormat:@"CGInset: { top:%f, right:%f, bottom:%f, left:%f }", aValue.top, aValue.right, aValue.bottom, aValue.left]; + else + { + description = "Object\n{\n"; + + for (var property in aValue) + { + if (aValue.hasOwnProperty(property)) + description += " " + property + ":" + aValue[property] + "\n"; + } + + description += "}"; + } + } + else + description = "Unknown object"; + } + else + description = [aValue description]; + + return description; +} + +// Returns undefined if $CAPP_BUILD is not defined, false if path cannot be found in $CAPP_BUILD +- (id)findResourceInCappBuild:(CPString)path isDirectory:(BOOL)isDirectory callback:(SEL)callback +{ + var cappBuild = SYS.env["CAPP_BUILD"]; + + if (!cappBuild) + return undefined; + + cappBuild = FILE.canonical(cappBuild); + + if (FILE.isDirectory(cappBuild)) + { + var result = null; + + for (var i = 0; i < BuildTypes.length && !result; ++i) + { + var findPath = FILE.join(cappBuild, BuildTypes[i], path); + + if ((isDirectory && FILE.isDirectory(findPath)) || (!isDirectory && FILE.exists(findPath))) + result = [self performSelector:callback withObject:findPath]; + } + + return result; + } + else + return false; +} + +- (CPString)findResourceInInstalledFrameworks:(CPString)path isDirectory:(BOOL)isDirectory callback:(SEL)callback +{ + // NOTE: It's safe to use '/' directly in the path, we're guaranteed to be on a Mac + var frameworks = FILE.canonical(FILE.join(SYS.prefix, "packages/cappuccino/Frameworks")), + result = null, + findPath = FILE.join(frameworks, "Debug", path); + + if ((isDirectory && FILE.isDirectory(findPath)) || (!isDirectory && FILE.exists(findPath))) + result = [self performSelector:callback withObject:findPath]; + + if (!result) + { + findPath = FILE.join(frameworks, path); + + if ((isDirectory && FILE.isDirectory(findPath)) || (!isDirectory && FILE.exists(findPath))) + result = [self performSelector:callback withObject:findPath]; + } + + return result; +} + +- (CPString)getAppKitDefaultThemeName +{ + var callback = @selector(themeNameFromPropertyListAtPath:), + themeName = [self findResourceInCappBuild:@"AppKit/Info.plist" isDirectory:NO callback:callback]; + + if (!themeName) + themeName = [self findResourceInInstalledFrameworks:@"AppKit/Info.plist" isDirectory:NO callback:callback]; + + return themeName || DefaultTheme; +} + +- (CPString)themeNameFromPropertyListAtPath:(CPString)path +{ + if (!FILE.isReadable(path)) + return nil; + + var themeName = nil, + plist = CFPropertyList.readPropertyListFromFile(path); + + if (plist) + themeName = plist.valueForKey("CPDefaultTheme"); + + return themeName; +} + +- (CPTheme)loadTheme:(CPString)themeName fromDirectory:(CPString)themeDir +{ + if (/^.+\.blend$/.test(themeName)) + themeName = themeName.substr(0, themeName.length - ".blend".length); + + var blendName = themeName + ".blend", + themePath = ""; + + if (themeDir) + { + themePath = FILE.join(FILE.canonical(themeDir), blendName); + + if (!FILE.isDirectory(themePath)) + themePath = themeDir = null; + } + + if (!themeDir) + { + var callback = @selector(returnPath:); + + themePath = [self findResourceInCappBuild:blendName isDirectory:YES callback:callback]; + + if (!themePath) + themePath = [self findResourceInInstalledFrameworks:@"AppKit/Resources/" + blendName isDirectory:YES callback:callback]; + + // Last resort, try the cwd + if (!themePath) + { + var path = FILE.canonical(blendName); + + if (FILE.isDirectory(path)) + themePath = path; + } + } + + if (!themePath) + [CPException raise:DumpThemeException reason:@"Cannot find the theme \"" + themeName + '"']; + + return [self readTheme:themeName atPath:themePath]; +} + +- (CPString)returnPath:(CPString)path +{ + return path; +} + +- (CPTheme)readTheme:(CPString)themeName atPath:(CPString)themePath +{ + var keyedTheme = null, + staticData = FILE.read(FILE.join(themePath, "Browser.Environment", FILE.basename(themePath) + ".sj")); + + if (!staticData) + [CPException raise:DumpThemeException reason:@"Could not find the theme file: " + themePath]; + + try + { + keyedTheme = [self readStaticResource:themeName + @".keyedtheme" fromData:staticData]; + + if (!keyedTheme) + [CPException raise:DumpThemeException reason:@"Could not find the keyed theme data in the theme at: " + themePath]; + } + catch (ex) + { + if (![ex isKindOfClass:CPException]) + [CPException raise:DumpThemeException reason:@"Could not read the theme at: " + themePath]; + } + + keyedTheme = CFPropertyList.propertyListFromString(keyedTheme); + + var data = [CPData dataWithPlistObject:keyedTheme], + theme = [CPKeyedUnarchiver unarchiveObjectWithData:data]; + + if (!theme) + [CPException raise:DumpThemeException reason:@"Could not unarchive the theme at: " + themePath]; + + colorPrint("purple", "Loaded theme: " + themePath + "\n"); + + return theme; +} + +- (JSObject)readStaticResource:(CPString)name fromData:(CPString)staticData +{ + name = @"Resources/" + name; + + // After the header is the version + var version = [self readFloatItemFromData:staticData]; + + while (true) + { + var resourceName = [self readStringItemFromData:staticData]; + + mark += 2; // skip "t;" + + var resourceLength = [self readIntItemFromData:staticData]; + + if (resourceName == name) + return [self readDataItemFromData:staticData length:resourceLength]; + + mark += resourceLength; + + if (staticData.substr(mark, 2) == "e;") + return null; + } +} + +- (int)readIntItemFromData:(CPString)staticData +{ + var endPos = [self endOfItemInData:staticData], + value = parseInt(staticData.substring(mark, endPos), 10); + + mark = endPos + 1; + + return value; +} + +- (float)readFloatItemFromData:(CPString)staticData +{ + var endPos = [self endOfItemInData:staticData], + value = parseFloat(staticData.substring(mark, endPos)); + + mark = endPos + 1; + + return value; +} + +- (CPString)readStringItemFromData:(CPString)staticData +{ + if (staticData.substr(mark, 2) !== "p;") + [CPException raise:DumpThemeException reason:@"Expected string item, found '" + staticData.substr(mark, 2) + @"'"]; + + mark += 2; // skip "p;" + + var length = [self readIntItemFromData:staticData], + value = staticData.substr(mark, length); + + mark += length; + + return value; +} + +- (int)endOfItemInData:(CPString)staticData +{ + var endPos = staticData.indexOf(";", mark); + + if (endPos < 0) + [CPException raise:DumpThemeException reason:@"Could not find item terminator"]; + + return endPos; +} + +- (CPString)readDataItemFromData:staticData length:(int)dataLength +{ + var data = staticData.substr(mark, dataLength); + + mark += dataLength; + + return data; +} + +@end + +function colorPrint(color, message) +{ + if (options.colorize) + stream.print("\0bold(\0" + color + "(" + message + "\0)\0)"); + else + stream.print(message); +} diff --git a/Tools/dump_theme/dump_theme.1 b/Tools/dump_theme/dump_theme.1 new file mode 100644 index 000000000..7bbab2631 --- /dev/null +++ b/Tools/dump_theme/dump_theme.1 @@ -0,0 +1,48 @@ +.Dd May 5, 2011 +.Os "Cappuccino" +.Dt DUMP_THEME 1 "PRM" +.\"----------------------------------------------------------------------------------------- +.Sh NAME +.\"----------------------------------------------------------------------------------------- +.Nm dump_theme +.Nd formatted dump of a Cappuccino theme +.\"----------------------------------------------------------------------------------------- +.Sh SYNOPSIS +.\"----------------------------------------------------------------------------------------- +.Nm +.Op --no-color +.Op Ar name | blend-path +.\"----------------------------------------------------------------------------------------- +.Sh "DESCRIPTION" +.\"----------------------------------------------------------------------------------------- +.Nm +prints a formatted dump of the contents of a Cappuccino theme. +.Pp +If you don't pass any arguments, +.Nm +will attempt to find the default AppKit theme from AppKit's Info.plist. If that cannot be +found, "Aristo" is used. +.Pp +If you pass a theme name only (without a ".blend" suffix), +.Nm +will attempt to locate the named theme blend in your $CAPP_BUILD and install frameworks. +.Pp +If you pass a theme blend path (thats end with ".blend"), +.Nm +will dump the theme at that location. +.\"----------------------------------------------------------------------------------------- +.Sh "OPTIONS" +.\"----------------------------------------------------------------------------------------- +The following options are available: +.Bl -tag -width 4n +.It Fl \-no-color +Normally +.Nm +uses in addition to indentation to make it easier to read the output. You can turn +colorizing off by passing this option. +.El +.\"----------------------------------------------------------------------------------------- +.Sh "RETURN VALUES" +.\"----------------------------------------------------------------------------------------- +.Nm +returns 0 for a successful conversion and >0 if an error occurred.