diff --git a/Package.php b/Package.php new file mode 100644 index 0000000..75cbbf8 --- /dev/null +++ b/Package.php @@ -0,0 +1,105 @@ +path = $this->resolve_path($package_path); + $this->root_dir = dirname($this->path); + $this->parse($this->path); + } + } + + public function __toString() + { + return $this->get_name(); + } + + static function decode($path){ + return preg_match('/\.json$/', $path) ? json_decode(file_get_contents($path), true) : YAML::decode_file($path); + } + + static function glob($path, $pattern = '*', $flags = 0, $depth = 0){ + $matches = array(); + $folders = array(rtrim($path, DIRECTORY_SEPARATOR)); + + while ($folder = array_shift($folders)){ + $matches = array_merge($matches, glob($folder.DIRECTORY_SEPARATOR.$pattern, $flags)); + + if (!$depth) continue; + + $moreFolders = glob($folder.DIRECTORY_SEPARATOR.'*', GLOB_ONLYDIR); + $depth = ($depth < -1) ? -1: $depth + count($moreFolders) - 2; + $folders = array_merge($folders, $moreFolders); + } + return $matches; + } + + public function add_source($source_path = '') + { + if (!is_a($source_path, 'Source')) $source_path = new Source($this->get_name(), $source_path); + $this->sources[] = $source_path; + } + + public function get_name() + { + return $this->name; + } + + public function get_path() + { + return $this->path; + } + + public function get_sources() + { + return $this->sources; + } + + public function parse($package_path) + { + $package = self::decode($package_path); + + foreach ($package as $key => $value){ + $method = 'parse_' . strtolower($key); + if (is_callable(array($this, $method))) $this->$method($value); + } + } + + public function parse_name($name) + { + $this->set_name($name); + } + + public function parse_sources($sources) + { + # todo(ibolmo): 5, should be a class option. + if (is_string($sources)) $sources = self::glob($this->path, $sources, 0, 5); + foreach ($sources as $source) $this->add_source($this->root_dir . '/' . $source); + } + + public function set_name($name) + { + $this->name = $name; + return $this; + } + + public function resolve_path($path){ + if (!is_dir($path) && file_exists($path)) return $path; + + $pathinfo = pathinfo($path); + $path = $pathinfo['dirname'] . '/' . $pathinfo['basename'] . '/'; + + if (file_exists($path . 'package.yml')) return $path . 'package.yml'; + if (file_exists($path . 'package.yaml')) return $path . 'package.yaml'; + if (file_exists($path . 'package.json')) return $path . 'package.json'; + + throw new Exception("package.(ya?ml|json) not found in $path."); + } +} diff --git a/Packager.php b/Packager.php new file mode 100644 index 0000000..bc40693 --- /dev/null +++ b/Packager.php @@ -0,0 +1,174 @@ +configure(); + if ($package_paths) foreach ((array) $package_paths as $package) $this->add_package($package); + } + + static function get_instance() + { + if (!self::$instance) self::$instance = new Packager(); + return self::$instance; + } + + static function strip_blocks($code, $blocks) + { + foreach ((array) $blocks as $block){ + $code = preg_replace_callback("%(/[/*])\s*<$block>(.*?)(?:\s*\*/)?%s", function($matches){ + return (strpos($matches[2], ($matches[1] == "//") ? "\n" : "*/") === false) ? $matches[2] : ""; + }, $code); + } + return $code; + } + + public function add_component(Source $source, $component) + { + $index = $this->get_source_index($source); + if ($index < 0) $index = $this->add_source($source); + + foreach ($this->generators as $name => $callback){ + $key = call_user_func($callback, $source, $component); + $this->set_key($key, $index, $name); + } + + return $index; + } + + public function add_generator($name, $callback) + { + $this->generators[$name] = $callback; + } + + public function add_package($package) + { + if (!is_a($package, 'Package')) $package = new Package($package); + $this->packages[$package->get_name()] = $package; + } + + public function add_source(Source $source) + { + $index = array_push($this->sources, $source) - 1; + return $this->keys[$source->get_name()] = $index; + } + + public function build(Source $source) + { + $build = array_map(function($source){ + return $source->get_code(); + }, $this->get_required_for_source($source)); + $build[] = $source->get_code(); + return implode('', $build); + } + + public function configure() + { + $this->add_generator('component name', function(Source $source, $component){ + return $component; + }); + + $this->add_generator('package and source name', function(Source $source, $component){ + return sprintf('%s/%s', $source->get_package_name(), $source->get_name()); + }); + + $this->add_generator('package and component name', function(Source $source, $component){ + return sprintf('%s/%s', $source->get_package_name(), $component); + }); + } + + public function get_packages() + { + return $this->packages; + } + + public function get_source_by_name($name) + { + $index = $this->get_source_index($name); + if ($index > -1) return $this->sources[$index]; + + $this->warn("Could not find source '$name'."); + return null; + } + + public function get_source_index($key) + { + if ($key instanceof Source) $key = $key->get_name(); + return isset($this->keys[$key]) ? $this->keys[$key] : -1; + } + + public function get_sources() + { + return $this->sources; + } + + public function get_required_for_source(Source $source, &$required = null) + { + $return = false; + if (!$required){ + $return = true; + $required = array(); + } + + foreach ($source->get_requires() as $require){ + if (!($require instanceof Source)) $require = $this->get_source_by_name($require); + if (!$require) continue; + if ($require->has_requires()) $this->get_required_for_source($require, $required); + } + foreach ($source->get_requires() as $require){ + if (!($require instanceof Source)) $require = $this->get_source_by_name($require); + if (!$require) continue; + if (!in_array($require, $required)) $required[] = $require; + } + + if ($return) return $required; + } + + public function remove_component(Source $source, $component) + { + foreach ($this->generators as $name => $callback){ + $key = call_user_func($callback, $source, $component); + unset($this->keys[$key]); + } + } + + public function remove_package($package_name) + { + if ($package_name instanceof Package) $package_name = $package_name->get_name(); + if (!isset($this->packages[$package_name])) return false; + foreach($this->packages[$package_name]->get_sources() as $source) $this->remove_source($source); + unset($this->packages[$package_name]); + return true; + } + + public function remove_source(Source $source) + { + $name = $source->get_name(); + $index = $this->keys[$name]; + foreach ($source->get_provides() as $component) $this->remove_component($source, $component); + unset($this->keys[$name], $this->sources[$index]); + } + + protected function set_key($key, $index, $generator_name) + { + if (isset($this->keys[$key])) $this->warn("Generator '$generator_name' set component key '$key'."); + $this->keys[$key] = $index; + } + + protected function warn($message) + { + # todo(ibolmo): log mixin + } + +} diff --git a/README.md b/README.md index 97f46c9..4f96e7c 100644 --- a/README.md +++ b/README.md @@ -1,160 +1,45 @@ Packager ======== -Packager is a PHP 5.2+ library to concatenate libraries split in multiple files in a single file. It automatically calculates dependancies. Packager requires a yml header syntax in every file, and a `package.yml` manifest file, as seen on the MooTools project. +Packager is a PHP 5.3+ library to concatenate libraries split in multiple files in a single file. It automatically calculates dependancies. -Packager API -============ +Packager requires a yml header syntax in every file, and a `package.yml` manifest file, as seen on the MooTools project. -Constructor ------------ +2.0 Branch +---------- +This is an experimental branch, dedicated to a more programmatic approach to packaging (building) your JavaScript files. -The constructor of this class accepts either a path to a package or a list of path to packages. `package.yml` must not be included in the path. +With the previous Packager, you were **required** to define a `package.yml`. This is no longer the case. You're still able to work with your `package.json` or `package.yml` files, but you have the ability to define dependencies at runtime and build only the dependencies for that file. -### Example - - $pkg = new Packager("/Users/kamicane/Sites/mootools-core/"); - $pkg = new Packager(array("/Users/kamicane/Sites/mootools-core/", "/Users/kamicane/Sites/mootools-more/")); +Why? +---- +I needed a more dynamic packager. On my web development, each web page varies between JavaScript uses. My home page, for example, might have a login area but the about us page does not. Why should I build a single `mootools.js` for the whole site? Even if I could keep track of what pages require what, it's not my job or the application's job to keep that in order. -Adding a manifest ------------------ +Instead, I wanted to: ``. I wanted homepage.js to have a yml header, and for Packager to build a _specific build_ for this **specific page**. The next step would be to create agent specific builds, but that's for another branch. -* `parse_manifest` » adds a manifest to this instance +API 2.0 +======= +Remember, the emphasis has been to create an actual API. The following are classes and their purpose. See the linked wiki pages, for additional method signatures and documentation. +[Packager](#) +Packager is the "registry" of components, and the manager of building the dependencies. -Working with files ------------------- +[Package](#) +A Package is a container of many sources. Due to the emphasis on Source, Package is not as useful **yet**. -### Getters +[Source](#) +A Source has many components (provides) and dependencies (requires). By defining a Source, you can add (provide) to the registry and build all dependencies for the source. - * `get_all_files` » gets an list of all files - * `get_file_dependancies` » gets an ordered list of every file that this file depends on - * `get_file_path` » gets the file path - * `get_file_source` » gets the file source - * `get_file_description` » gets the file description - * `get_file_provides` » gets a list of the file provided components +[Command Line Script](#) +This is currently broken, and low priority for me. Pull requests, greatly appreciated. -### Converters +Since I'm using Packager 2.0 as an API I no longer need a `npm|gem|pear`-like library for my development. - * `complete_file` » converts a single file to an ordered list of files - * `complete_files` » converts a list of files to an ordered list of files -### Generators +Getting Stated +============== +banana banana banana - * `build_from_files` » returns a string containing the source of the selected files and their dependancies - * `write_from_files` » writes a file with the selected files and their dependancies +For now, take a look at [sfPackagerPlugin](https://github.com/ibolmo/sfPackagerPlugin) for a programmatic usage of the 2.0 branch. In particular take a look at: [PackagerHelper](https://github.com/ibolmo/sfPackagerPlugin/blob/master/lib/helper/PackagerHelper.php) which has the "useful" interface, I had been talking about. -Working with components ------------------------ - -### Converting to files - - * `component_to_file` » gets the name of the file that provides this component - * `components_to_files` » converts a list of components to a list of files - -### Generators - - * `build_from_components` » returns a string containing the source of the selected components and their dependancies - * `write_from_components` » writes a file with the selected components and their dependancies - - -Class usage ------------ - -### Syntax - - $pkg = new Packager(`$path_to_manifest`); - -### Example - - $pkg = new Packager("/Users/kamicane/Sites/mootools-core/"); - - $pkg->write_from_components("/Users/kamicane/Sites/mootools.js", array('Type', 'Array')); - -Packager Command Line script ----------------------------- - -The Packager command line script is your one-stop solution to build any of your packages at once. Works on unices. - -### Syntax - - ./packager COMMAND +option1 argument1 argument2 +option2 argument1 argument2 - -* `COMMAND` a packager command *(required)* -* `+option` options for commands *(optional)* - -### Commands - -* `register` registers a package. Creates a .packages.yml file in your home folder. -* `unregister` unregisters a package -* `list` list registered packages -* `build` builds a single file with the supplied packages / files / components - -### Registering a Package - -#### Example - - ./packager register /Users/kamicane/mootools-core - » the package Core has been registered - - -### Listing Packages - -#### Example - - ./packager list - » Core: /Users/kamicane/mootools-core - - -### Unregistering a Package - -#### Example - - ./packager unregister Core - » the package Core has been unregistered - -### Building Packages - -#### Examples - - ./packager build Core/Type Core/Fx ART/ART.Element - -Which is the same as... - - ./packager build +components Core/Type Core/Fx ART/ART.Element - -Which builds the passed in components (and their dependancies) using your registered packages. - - ./packager build +files Core/Core Core/Fx ART/ART - -This builds the passed in files (and their dependancies) using your registered packages. - - ./packager build ART/* - -Builds every component from ART, and their dependancies, using your registered packages. - - ./packager build SomePackage/SomeComponent +packages /Users/kamicane/Sites/some-package - -Builds the selected components using your registered packages and a temporary package that resides in /Users/kamicane/Sites/some-package, without having to register it first. - - ./packager build SomePackage/SomeComponent -packages Core - -Builds the selected components using your registered packages minus the package names you pass to -packages. This lets you build your components without dependancies. - - ./packager build ART/SomeComponent +use-only ART - -Builds the selected components using only ART of your registered packages. This lets you build your components without dependancies. - - ./packager build SomePackage/SomeComponent +use-only +packages /Users/kamicane/Sites/some-package - -Builds the selected components using none of your registered packages plus the passed in package, without registering it. This lets you build your components without dependancies. - - ./packager build +components ART/ART +files ART/ART.Base - -You can mix components and files - - ./packager build Core/* > mootools.js - -This is how you output to a file - diff --git a/Source.php b/Source.php new file mode 100644 index 0000000..80eddbc --- /dev/null +++ b/Source.php @@ -0,0 +1,143 @@ +package_name = $package_name; + if ($source_path) $this->parse($source_path); + } + + public function __toString() + { + return $this->get_name(); + } + + static function normalize_name($default, $name){ + $exploded = explode('/', $name); + if (count($exploded) == 1) return array($default, $exploded[0]); + if (empty($exploded[0])) return array($default, $exploded[1]); + return array($exploded[0], $exploded[1]); + } + + public function build() + { + return Packager::get_instance()->build($this); + } + + public function get_code() + { + return $this->code; + } + + public function get_name() + { + if (!$this->name) $this->name = basename($this->path, '.js'); + if (!$this->name) $this->name = $this->package_name; + return $this->name; + } + + public function get_package_name() + { + return $this->package_name; + } + + public function get_provides() + { + return $this->provides; + } + + public function get_requires() + { + return $this->requires; + } + + public function has_requires() + { + return !empty($this->requires); + } + + public function parse($source_path = '') + { + if ($source_path){ + $this->path = $source_path; + $this->code = file_get_contents($source_path); + } + + if (!$this->code) throw new RuntimeException('Missing the code to parse. Did you forget to supply the source_path or set_code?'); + + preg_match(self::DESCRIPTOR_REGEX, $this->code, $matches); + + if (!empty($matches)){ + $header = YAML::decode($matches[0]); + + foreach ($header as $key => $value){ + $method = 'parse_' . strtolower($key); + if (is_callable(array($this, $method))) $this->$method($value); + } + } else { + $this->warn("No yaml header present in $source_path"); + } + } + + public function parse_name($name) + { + $this->name = $name; + } + + public function parse_provides($provides) + { + $provides = (array) $provides; + $this->provides($provides); + } + + public function parse_requires($requires) + { + $requires = (array) $requires; + foreach ($requires as $i => $require) $requires[$i] = implode('/', self::normalize_name($this->package_name, $require)); + $this->requires($requires); + } + + public function provides($provides) + { + $packager = Packager::get_instance(); + foreach ((array) $provides as $component){ + $packager->add_component($this, $component); + $this->provides[] = $component; + } + return $this; + } + + public function requires($requires) + { + $this->requires = (array) $requires; + return $this; + } + + public function set_name($name) + { + $this->name = $name; + return $this; + } + + public function set_code($code) + { + $this->code = $code; + return $this; + } + + public function warn($message) + { + # todo(ibolmo): log mixin + } +} diff --git a/helpers/yaml.php b/helpers/yaml.php index 7fdf1f7..816ac8d 100644 --- a/helpers/yaml.php +++ b/helpers/yaml.php @@ -1,6 +1,6 @@ get_package_name(); - $path = $pkg->get_package_manifest(); + $pkg = new Package($realpath); + $name = $pkg->get_name(); + $path = $pkg->get_path(); $packages[$name] = $path; file_put_contents("$home/.packages.yml", YAML::encode($packages)); - - Packager::warn("the package $name has been registered as $path\n"); + + info("the package $name has been registered as $path\n"); break; @@ -46,7 +60,7 @@ switch ($command){ $name = array_shift($argv); if (empty($packages[$name])){ - Packager::warn("there is no package called $name\n"); + warn("there is no package called $name\n"); break; } @@ -54,7 +68,7 @@ switch ($command){ file_put_contents("$home/.packages.yml", YAML::encode($packages)); - Packager::warn("the package $name has been unregistered\n"); + info("the package $name has been unregistered\n"); break; @@ -63,18 +77,18 @@ switch ($command){ $package_name = array_shift($argv); if (empty($package_name)){ - foreach ($packages as $name => $path) Packager::warn("$name: $path\n"); + foreach ($packages as $name => $path) info("$name: $path\n"); } else { if (!empty($packages[$package_name])){ $package_path = $packages[$package_name]; - $pkg = new Packager($package_path); - $files = $pkg->get_all_files(); - foreach ($files as $file){ - $file_name = $pkg->get_file_name($file); - Packager::warn("- $file_name: [" . implode(", ", $pkg->get_file_provides($file)) . "]\n"); + $pkg = new Package($package_path); + $sources = $pkg->get_sources(); + foreach ($sources as $source) { + $name = $source->get_name(); + info("- $name: [" . implode(", ", $source->get_provides()) . "]\n"); } } else { - Packager::warn("The package $package_name has not been found.\n"); + warn("The package $package_name has not been found.\n"); } } @@ -135,10 +149,11 @@ switch ($command){ if (!$selected['use-only'] || array_contains($selected['use-only'], $name)) $paths[] = $path; } - $pkg = new Packager($paths); + $packager = Packager::get_instance(); + foreach ($paths as $path) $packager->add_package($path); - foreach ($selected['remove-packages'] as $package_name) $pkg->remove_package($package_name); - foreach ($selected['add-packages'] as $package_path) $pkg->add_package($package_path); + foreach ($selected['remove-packages'] as $package_name) $packager->remove_package($package_name); + foreach ($selected['add-packages'] as $package_path) $packager->add_package($package_path); $re = "/^([^\/]+)\/\*$/"; $wildcards = array(); @@ -163,31 +178,20 @@ switch ($command){ } } - $pkg->validate($files, $components, $wildcards); - - foreach ($components as $component){ - $file = $pkg->component_to_file($component); - if ($file) array_include($files, $file); - } - - foreach ($wildcards as $package){ - $all = $pkg->get_all_files($package); - foreach ($all as $file) array_include($files, $file); - } - - $files = $pkg->complete_files($files); + $source = new Source('packager-cli'); + $source->requires(array_merge($files, $wildcards)); + warn("Build using: " . implode(', ', $packager->get_packages()) . "\n"); + warn("Included Files/Components:\n"); - Packager::warn("Build using: " . implode(', ', $pkg->get_packages()) . "\n"); - - Packager::warn("Included Files/Components:\n"); - - foreach ($files as $file){ - $file_name = $pkg->get_file_name($file); - $file_package = $pkg->get_file_package($file); - Packager::warn("- $file_package/$file_name: [" . implode(", ", $pkg->get_file_provides($file)) . "]\n"); + $required = $packager->get_required_for_source($source); + foreach ($required as $required_source){ + $file_name = $required_source->get_name(); + $file_package = $required_source->get_package_name(); + + warn("- $file_package/$file_name: [" . implode(", ", $required_source->get_provides()) . "]\n"); } - echo $pkg->build($files, array(), array(), $blocks); + echo Packager::strip_blocks($source->build(), $blocks); break; @@ -204,7 +208,7 @@ switch ($command){ } function usage($command = ''){ - $dir = dirname(__FILE__); + $dir = __DIR__; if (empty($command) || !file_exists("$dir/help/$command.txt")) $command = 'default'; echo file_get_contents("$dir/help/$command.txt"); } diff --git a/packager.php b/packager.php deleted file mode 100644 index b5ecb32..0000000 --- a/packager.php +++ /dev/null @@ -1,341 +0,0 @@ -parse_manifest($package_path); - } - - private function parse_manifest($path){ - $pathinfo = pathinfo($path); - - if (is_dir($path)){ - - $package_path = $pathinfo['dirname'] . '/' . $pathinfo['basename'] . '/'; - - if (file_exists($package_path . 'package.yml')){ - $manifest_path = $package_path . 'package.yml'; - $manifest_format = 'yaml'; - } else if (file_exists($package_path . 'package.yaml')){ - $manifest_path = $package_path . 'package.yaml'; - $manifest_format = 'yaml'; - } else if (file_exists($package_path . 'package.json')){ - $manifest_path = $package_path . 'package.json'; - $manifest_format = 'json'; - } - - } else if (file_exists($path)){ - $package_path = $pathinfo['dirname'] . '/'; - $manifest_path = $package_path . $pathinfo['basename']; - $manifest_format = $pathinfo['extension']; - } - - if ($manifest_format == 'json') $manifest = json_decode(file_get_contents($manifest_path), true); - else if ($manifest_format == 'yaml' || $manifest_format == 'yml') $manifest = YAML::decode_file($manifest_path); - - if (empty($manifest)) throw new Exception("manifest not found in $package_path, or unable to parse manifest."); - - $package_name = $manifest['name']; - - if ($this->root == null) $this->root = $package_name; - - if (array_has($this->manifests, $package_name)) return; - - $manifest['path'] = $package_path; - $manifest['manifest'] = $manifest_path; - - $this->manifests[$package_name] = $manifest; - - foreach ($manifest['sources'] as $i => $path){ - - $path = $package_path . $path; - - // this is where we "hook" for possible other replacers. - $source = file_get_contents($path); - - $descriptor = array(); - - // get contents of first comment - preg_match('/\/\*\s*^---(.*?)^\.\.\.\s*\*\//ms', $source, $matches); - - if (!empty($matches)) $descriptor = YAML::decode($matches[0]); - - // populate / convert to array requires and provides - $requires = (array)(!empty($descriptor['requires']) ? $descriptor['requires'] : array()); - $provides = (array)(!empty($descriptor['provides']) ? $descriptor['provides'] : array()); - $file_name = !empty($descriptor['name']) ? $descriptor['name'] : basename($path, '.js'); - - // "normalization" for requires. Fills up the default package name from requires, if not present. - foreach ($requires as $i => $require) - $requires[$i] = implode('/', $this->parse_name($package_name, $require)); - - $license = array_get($descriptor, 'license'); - - $this->packages[$package_name][$file_name] = array_merge($descriptor, array( - 'package' => $package_name, - 'requires' => $requires, - 'provides' => $provides, - 'source' => $source, - 'path' => $path, - 'package/name' => $package_name . '/' . $file_name, - 'license' => empty($license) ? array_get($manifest, 'license') : $license - )); - - } - } - - public function add_package($package_path){ - $this->parse_manifest($package_path); - } - - public function remove_package($package_name){ - unset($this->packages[$package_name]); - unset($this->manifests[$package_name]); - } - - // # private UTILITIES - - private function parse_name($default, $name){ - $exploded = explode('/', $name); - $length = count($exploded); - if ($length == 1) return array($default, $exploded[0]); - if (empty($exploded[0])) return array($default, $exploded[1]); - return array($exploded[0], $exploded[1]); - } - - // # private HASHES - - private function component_to_hash($name){ - $pair = $this->parse_name($this->root, $name); - $package = array_get($this->packages, $pair[0]); - - if (!empty($package)){ - $component = $pair[1]; - - foreach ($package as $file => $data){ - foreach ($data['provides'] as $c){ - if ($c == $component) return $data; - } - } - } - - return null; - } - - private function file_to_hash($name){ - $pair = $this->parse_name($this->root, $name); - $package = array_get($this->packages, $pair[0]); - - if (!empty($package)){ - $file_name = $pair[1]; - - foreach ($package as $file => $data){ - if ($file == $file_name) return $data; - } - } - - return null; - } - - public function file_exists($name){ - return $this->file_to_hash($name) ? true : false; - } - - public function component_exists($name){ - return $this->component_to_hash($name) ? true : false; - } - - public function package_exists($name){ - return array_contains($this->get_packages(), $name); - } - - public function validate($more_files = array(), $more_components = array(), $more_packages = array()){ - - foreach ($this->packages as $name => $files){ - foreach ($files as $file){ - $file_requires = $file['requires']; - foreach ($file_requires as $component){ - if (!$this->component_exists($component)){ - self::warn("WARNING: The component $component, required in the file " . $file['package/name'] . ", has not been provided.\n"); - } - } - } - } - - foreach ($more_files as $file){ - if (!$this->file_exists($file)) self::warn("WARNING: The required file $file could not be found.\n"); - } - - foreach ($more_components as $component){ - if (!$this->component_exists($component)) self::warn("WARNING: The required component $component could not be found.\n"); - } - - foreach ($more_packages as $package){ - if (!$this->package_exists($package)) self::warn("WARNING: The required package $package could not be found.\n"); - } - } - - // # public BUILD - - public function build($files = array(), $components = array(), $packages = array(), $blocks = array()){ - - if (!empty($components)){ - $more = $this->components_to_files($components); - foreach ($more as $file) array_include($files, $file); - } - - foreach ($packages as $package){ - $more = $this->get_all_files($package); - foreach ($more as $file) array_include($files, $file); - } - - $files = $this->complete_files($files); - - if (empty($files)) return ''; - - $included_sources = array(); - foreach ($files as $file) $included_sources[] = $this->get_file_source($file); - - $source = implode($included_sources, "\n\n"); - - foreach ($blocks as $block){ - $source = preg_replace_callback("%(/[/*])\s*<$block>(.*?)(?:\s*\*/)?%s", array($this, "block_replacement"), $source); - } - - return $source . "\n"; - } - - private function block_replacement($matches){ - return (strpos($matches[2], ($matches[1] == "//") ? "\n" : "*/") === false) ? $matches[2] : ""; - } - - public function build_from_files($files){ - return $this->build($files); - } - - public function build_from_components($components){ - return $this->build(array(), $components); - } - - public function write_from_files($file_name, $files = null){ - $full = $this->build_from_files($files); - file_put_contents($file_name, $full); - } - - public function write_from_components($file_name, $components = null){ - $full = $this->build_from_components($components); - file_put_contents($file_name, $full); - } - - // # public FILES - - public function get_all_files($of_package = null){ - $files = array(); - foreach ($this->packages as $name => $package){ - if ($of_package == null || $of_package == $name) foreach ($package as $file){ - $files[] = $file['package/name']; - } - } - return $files; - } - - public function get_file_dependancies($file){ - $hash = $this->file_to_hash($file); - if (empty($hash)) return array(); - return $this->complete_files($this->components_to_files($hash['requires'])); - } - - public function complete_file($file){ - $files = $this->get_file_dependancies($file); - $hash = $this->file_to_hash($file); - if (empty($hash)) return array(); - array_include($files, $hash['package/name']); - return $files; - } - - public function complete_files($files){ - $ordered_files = array(); - foreach ($files as $file){ - $all_files = $this->complete_file($file); - foreach ($all_files as $one_file) array_include($ordered_files, $one_file); - } - return $ordered_files; - } - - // # public COMPONENTS - - public function component_to_file($component){ - return array_get($this->component_to_hash($component), 'package/name'); - } - - public function components_to_files($components){ - $files = array(); - foreach ($components as $component){ - $file_name = $this->component_to_file($component); - if (!empty($file_name) && !in_array($file_name, $files)) $files[] = $file_name; - } - return $files; - } - - // # dynamic getter for PACKAGE properties and FILE properties - - public function __call($method, $arguments){ - if (strpos($method, 'get_file_') === 0){ - $file = array_get($arguments, 0); - if (empty($file)) return null; - $key = substr($method, 9); - $hash = $this->file_to_hash($file); - return array_get($hash, $key); - } - - if (strpos($method, 'get_package_') === 0){ - $key = substr($method, 12); - $package = array_get($arguments, 0); - $package = array_get($this->manifests, (empty($package)) ? $this->root : $package); - return array_get($package, $key); - } - - return null; - } - - public function get_packages(){ - return array_keys($this->packages); - } - - // authors normalization - - public function get_package_authors($package = null){ - if (empty($package)) $package = $this->root; - $package = array_get($this->manifests, $package); - if (empty($package)) return array(); - return $this->normalize_authors(array_get($package, 'authors'), array_get($package, 'author')); - } - - public function get_file_authors($file){ - $hash = $this->file_to_hash($file); - if (empty($hash)) return array(); - return $this->normalize_authors(array_get($hash, 'authors'), array_get($hash, 'author'), $this->get_package_authors()); - } - - private function normalize_authors($authors = null, $author = null, $default = null){ - $use = empty($authors) ? $author : $authors; - if (empty($use) && !empty($default)) return $default; - if (is_array($use)) return $use; - if (empty($use)) return array(); - return array($use); - } - -} diff --git a/test/fixtures/Source/Array.js b/test/fixtures/Source/Array.js new file mode 100644 index 0000000..feee361 --- /dev/null +++ b/test/fixtures/Source/Array.js @@ -0,0 +1,176 @@ +/* +--- + +name: Array + +description: Contains Array Prototypes like each, contains, and erase. + +license: MIT-style license. + +requires: Type + +provides: Array + +... +*/ + +Array.implement({ + + invoke: function(methodName){ + var args = Array.slice(arguments, 1); + return this.map(function(item){ + return item[methodName].apply(item, args); + }); + }, + + every: function(fn, bind){ + for (var i = 0, l = this.length; i < l; i++){ + if ((i in this) && !fn.call(bind, this[i], i, this)) return false; + } + return true; + }, + + filter: function(fn, bind){ + var results = []; + for (var i = 0, l = this.length; i < l; i++){ + if ((i in this) && fn.call(bind, this[i], i, this)) results.push(this[i]); + } + return results; + }, + + clean: function(){ + return this.filter(function(item){ + return item != null; + }); + }, + + indexOf: function(item, from){ + var len = this.length; + for (var i = (from < 0) ? Math.max(0, len + from) : from || 0; i < len; i++){ + if (this[i] === item) return i; + } + return -1; + }, + + map: function(fn, bind){ + var results = []; + for (var i = 0, l = this.length; i < l; i++){ + if (i in this) results[i] = fn.call(bind, this[i], i, this); + } + return results; + }, + + some: function(fn, bind){ + for (var i = 0, l = this.length; i < l; i++){ + if ((i in this) && fn.call(bind, this[i], i, this)) return true; + } + return false; + }, + + associate: function(keys){ + var obj = {}, length = Math.min(this.length, keys.length); + for (var i = 0; i < length; i++) obj[keys[i]] = this[i]; + return obj; + }, + + link: function(object){ + var result = {}; + for (var i = 0, l = this.length; i < l; i++){ + for (var key in object){ + if (object[key](this[i])){ + result[key] = this[i]; + delete object[key]; + break; + } + } + } + return result; + }, + + contains: function(item, from){ + return this.indexOf(item, from) != -1; + }, + + append: function(array){ + this.push.apply(this, array); + return this; + }, + + getLast: function(){ + return (this.length) ? this[this.length - 1] : null; + }, + + getRandom: function(){ + return (this.length) ? this[Number.random(0, this.length - 1)] : null; + }, + + include: function(item){ + if (!this.contains(item)) this.push(item); + return this; + }, + + combine: function(array){ + for (var i = 0, l = array.length; i < l; i++) this.include(array[i]); + return this; + }, + + erase: function(item){ + for (var i = this.length; i--;){ + if (this[i] === item) this.splice(i, 1); + } + return this; + }, + + empty: function(){ + this.length = 0; + return this; + }, + + flatten: function(){ + var array = []; + for (var i = 0, l = this.length; i < l; i++){ + var type = typeOf(this[i]); + if (type == 'null') continue; + array = array.concat((type == 'array' || type == 'collection' || type == 'arguments' || instanceOf(this[i], Array)) ? Array.flatten(this[i]) : this[i]); + } + return array; + }, + + pick: function(){ + for (var i = 0, l = this.length; i < l; i++){ + if (this[i] != null) return this[i]; + } + return null; + }, + + hexToRgb: function(array){ + if (this.length != 3) return null; + var rgb = this.map(function(value){ + if (value.length == 1) value += value; + return value.toInt(16); + }); + return (array) ? rgb : 'rgb(' + rgb + ')'; + }, + + rgbToHex: function(array){ + if (this.length < 3) return null; + if (this.length == 4 && this[3] == 0 && !array) return 'transparent'; + var hex = []; + for (var i = 0; i < 3; i++){ + var bit = (this[i] - 0).toString(16); + hex.push((bit.length == 1) ? '0' + bit : bit); + } + return (array) ? hex : '#' + hex.join(''); + } + +}); + +//<1.2compat> + +Array.alias('extend', 'append'); + +var $pick = function(){ + return Array.from(arguments).pick(); +}; + +// diff --git a/test/fixtures/Source/Browser.js b/test/fixtures/Source/Browser.js new file mode 100644 index 0000000..93946be --- /dev/null +++ b/test/fixtures/Source/Browser.js @@ -0,0 +1,262 @@ +/* +--- + +name: Browser + +description: The Browser Object. Contains Browser initialization, Window and Document, and the Browser Hash. + +license: MIT-style license. + +requires: [Array] + +provides: [Browser, Window, Document] + +... +*/ + +(function(){ + +var document = this.document; +var window = document.window = this; + +var UID = 1; + +this.$uid = (window.ActiveXObject) ? function(item){ + return (item.uid || (item.uid = [UID++]))[0]; +} : function(item){ + return item.uid || (item.uid = UID++); +}; + +$uid(window); +$uid(document); + +var ua = navigator.userAgent.toLowerCase(), + platform = navigator.platform.toLowerCase(), + UA = ua.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/) || [null, 'unknown', 0], + mode = UA[1] == 'ie' && document.documentMode; + +var Browser = this.Browser = { + + extend: Function.prototype.extend, + + name: (UA[1] == 'version') ? UA[3] : UA[1], + + version: mode || parseFloat((UA[1] == 'opera' && UA[4]) ? UA[4] : UA[2]), + + Platform: { + name: ua.match(/ip(?:ad|od|hone)/) ? 'ios' : (ua.match(/(?:webos|android)/) || platform.match(/mac|win|linux/) || ['other'])[0] + }, + + Features: { + xpath: !!(document.evaluate), + air: !!(window.runtime), + query: !!(document.querySelector), + json: !!(window.JSON) + }, + + Plugins: {} + +}; + +Browser[Browser.name] = true; +Browser[Browser.name + parseInt(Browser.version, 10)] = true; +Browser.Platform[Browser.Platform.name] = true; + +// Request + +Browser.Request = (function(){ + + var XMLHTTP = function(){ + return new XMLHttpRequest(); + }; + + var MSXML2 = function(){ + return new ActiveXObject('MSXML2.XMLHTTP'); + }; + + var MSXML = function(){ + return new ActiveXObject('Microsoft.XMLHTTP'); + }; + + return Function.attempt(function(){ + XMLHTTP(); + return XMLHTTP; + }, function(){ + MSXML2(); + return MSXML2; + }, function(){ + MSXML(); + return MSXML; + }); + +})(); + +Browser.Features.xhr = !!(Browser.Request); + +// Flash detection + +var version = (Function.attempt(function(){ + return navigator.plugins['Shockwave Flash'].description; +}, function(){ + return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); +}) || '0 r0').match(/\d+/g); + +Browser.Plugins.Flash = { + version: Number(version[0] || '0.' + version[1]) || 0, + build: Number(version[2]) || 0 +}; + +// String scripts + +Browser.exec = function(text){ + if (!text) return text; + if (window.execScript){ + window.execScript(text); + } else { + var script = document.createElement('script'); + script.setAttribute('type', 'text/javascript'); + script.text = text; + document.head.appendChild(script); + document.head.removeChild(script); + } + return text; +}; + +String.implement('stripScripts', function(exec){ + var scripts = ''; + var text = this.replace(/]*>([\s\S]*?)<\/script>/gi, function(all, code){ + scripts += code + '\n'; + return ''; + }); + if (exec === true) Browser.exec(scripts); + else if (typeOf(exec) == 'function') exec(scripts, text); + return text; +}); + +// Window, Document + +Browser.extend({ + Document: this.Document, + Window: this.Window, + Element: this.Element, + Event: this.Event +}); + +this.Window = this.$constructor = new Type('Window', function(){}); + +this.$family = Function.from('window').hide(); + +Window.mirror(function(name, method){ + window[name] = method; +}); + +this.Document = document.$constructor = new Type('Document', function(){}); + +document.$family = Function.from('document').hide(); + +Document.mirror(function(name, method){ + document[name] = method; +}); + +document.html = document.documentElement; +document.head = document.getElementsByTagName('head')[0]; + +if (document.execCommand) try { + document.execCommand("BackgroundImageCache", false, true); +} catch (e){} + +if (this.attachEvent && !this.addEventListener){ + var unloadEvent = function(){ + this.detachEvent('onunload', unloadEvent); + document.head = document.html = document.window = null; + }; + this.attachEvent('onunload', unloadEvent); +} + +// IE fails on collections and ) +var arrayFrom = Array.from; +try { + arrayFrom(document.html.childNodes); +} catch(e){ + Array.from = function(item){ + if (typeof item != 'string' && Type.isEnumerable(item) && typeOf(item) != 'array'){ + var i = item.length, array = new Array(i); + while (i--) array[i] = item[i]; + return array; + } + return arrayFrom(item); + }; + + var prototype = Array.prototype, + slice = prototype.slice; + ['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift', 'concat', 'join', 'slice'].each(function(name){ + var method = prototype[name]; + Array[name] = function(item){ + return method.apply(Array.from(item), slice.call(arguments, 1)); + }; + }); +} + +//<1.2compat> + +if (Browser.Platform.ios) Browser.Platform.ipod = true; + +Browser.Engine = {}; + +var setEngine = function(name, version){ + Browser.Engine.name = name; + Browser.Engine[name + version] = true; + Browser.Engine.version = version; +}; + +if (Browser.ie){ + Browser.Engine.trident = true; + + switch (Browser.version){ + case 6: setEngine('trident', 4); break; + case 7: setEngine('trident', 5); break; + case 8: setEngine('trident', 6); + } +} + +if (Browser.firefox){ + Browser.Engine.gecko = true; + + if (Browser.version >= 3) setEngine('gecko', 19); + else setEngine('gecko', 18); +} + +if (Browser.safari || Browser.chrome){ + Browser.Engine.webkit = true; + + switch (Browser.version){ + case 2: setEngine('webkit', 419); break; + case 3: setEngine('webkit', 420); break; + case 4: setEngine('webkit', 525); + } +} + +if (Browser.opera){ + Browser.Engine.presto = true; + + if (Browser.version >= 9.6) setEngine('presto', 960); + else if (Browser.version >= 9.5) setEngine('presto', 950); + else setEngine('presto', 925); +} + +if (Browser.name == 'unknown'){ + switch ((ua.match(/(?:webkit|khtml|gecko)/) || [])[0]){ + case 'webkit': + case 'khtml': + Browser.Engine.webkit = true; + break; + case 'gecko': + Browser.Engine.gecko = true; + } +} + +this.$exec = Browser.exec; + +// + +}).call(this); diff --git a/test/fixtures/Source/Class.js b/test/fixtures/Source/Class.js new file mode 100644 index 0000000..9d45155 --- /dev/null +++ b/test/fixtures/Source/Class.js @@ -0,0 +1,115 @@ +/* +--- + +name: Class + +description: Contains the Class Function for easily creating, extending, and implementing reusable Classes. + +license: MIT-style license. + +requires: [Array] + +provides: Class + +... +*/ + +(function(){ + +var Class = this.Class = new Type('Class', function(params){ + if (instanceOf(params, Function)) params = {initialize: params}; + + var newClass = function(){ + reset(this); + if (newClass.$prototyping) return this; + this.$caller = null; + var value = (this.initialize) ? this.initialize.apply(this, arguments) : this; + this.$caller = this.caller = null; + return value; + }.extend(this).implement(params); + + newClass.$constructor = Class; + newClass.prototype.$constructor = newClass; + newClass.prototype.parent = parent; + + return newClass; +}); + +var parent = function(){ + if (!this.$caller) throw new Error('The method "parent" cannot be called.'); + var name = this.$caller.$name, + parent = this.$caller.$owner.parent, + previous = (parent) ? parent.prototype[name] : null; + if (!previous) throw new Error('The method "' + name + '" has no parent.'); + return previous.apply(this, arguments); +}; + +var reset = function(object){ + for (var key in object){ + var value = object[key]; + switch (typeOf(value)){ + case 'object': + var F = function(){}; + F.prototype = value; + object[key] = reset(new F); + break; + case 'array': object[key] = value.clone(); break; + } + } + return object; +}; + +var wrap = function(self, key, method){ + if (method.$origin) method = method.$origin; + var wrapper = function(){ + if (method.$protected && this.$caller == null) throw new Error('The method "' + key + '" cannot be called.'); + var caller = this.caller, current = this.$caller; + this.caller = current; this.$caller = wrapper; + var result = method.apply(this, arguments); + this.$caller = current; this.caller = caller; + return result; + }.extend({$owner: self, $origin: method, $name: key}); + return wrapper; +}; + +var implement = function(key, value, retain){ + if (Class.Mutators.hasOwnProperty(key)){ + value = Class.Mutators[key].call(this, value); + if (value == null) return this; + } + + if (typeOf(value) == 'function'){ + if (value.$hidden) return this; + this.prototype[key] = (retain) ? value : wrap(this, key, value); + } else { + Object.merge(this.prototype, key, value); + } + + return this; +}; + +var getInstance = function(klass){ + klass.$prototyping = true; + var proto = new klass; + delete klass.$prototyping; + return proto; +}; + +Class.implement('implement', implement.overloadSetter()); + +Class.Mutators = { + + Extends: function(parent){ + this.parent = parent; + this.prototype = getInstance(parent); + }, + + Implements: function(items){ + Array.from(items).each(function(item){ + var instance = new item; + for (var key in instance) implement.call(this, key, instance[key], true); + }, this); + } +}; + +}).call(this); diff --git a/test/fixtures/Source/Core.js b/test/fixtures/Source/Core.js new file mode 100644 index 0000000..69a11df --- /dev/null +++ b/test/fixtures/Source/Core.js @@ -0,0 +1,511 @@ +/* +--- + +name: Core + +description: The heart of MooTools. + +license: MIT-style license. + +copyright: Copyright (c) 2006-2010 [Valerio Proietti](http://mad4milk.net/). + +authors: The MooTools production team (http://mootools.net/developers/) + +inspiration: + - Class implementation inspired by [Base.js](http://dean.edwards.name/weblog/2006/03/base/) Copyright (c) 2006 Dean Edwards, [GNU Lesser General Public License](http://opensource.org/licenses/lgpl-license.php) + - Some functionality inspired by [Prototype.js](http://prototypejs.org) Copyright (c) 2005-2007 Sam Stephenson, [MIT License](http://opensource.org/licenses/mit-license.php) + +provides: [Core, MooTools, Type, typeOf, instanceOf, Native] + +... +*/ + +(function(){ + +this.MooTools = { + version: '1.3.2dev', + build: '%build%' +}; + +// typeOf, instanceOf + +var typeOf = this.typeOf = function(item){ + if (item == null) return 'null'; + if (item.$family) return item.$family(); + + if (item.nodeName){ + if (item.nodeType == 1) return 'element'; + if (item.nodeType == 3) return (/\S/).test(item.nodeValue) ? 'textnode' : 'whitespace'; + } else if (typeof item.length == 'number'){ + if (item.callee) return 'arguments'; + if ('item' in item) return 'collection'; + } + + return typeof item; +}; + +var instanceOf = this.instanceOf = function(item, object){ + if (item == null) return false; + var constructor = item.$constructor || item.constructor; + while (constructor){ + if (constructor === object) return true; + constructor = constructor.parent; + } + return item instanceof object; +}; + +// Function overloading + +var Function = this.Function; + +var enumerables = true; +for (var i in {toString: 1}) enumerables = null; +if (enumerables) enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'constructor']; + +Function.prototype.overloadSetter = function(usePlural){ + var self = this; + return function(a, b){ + if (a == null) return this; + if (usePlural || typeof a != 'string'){ + for (var k in a) self.call(this, k, a[k]); + if (enumerables) for (var i = enumerables.length; i--;){ + k = enumerables[i]; + if (a.hasOwnProperty(k)) self.call(this, k, a[k]); + } + } else { + self.call(this, a, b); + } + return this; + }; +}; + +Function.prototype.overloadGetter = function(usePlural){ + var self = this; + return function(a){ + var args, result; + if (usePlural || typeof a != 'string') args = a; + else if (arguments.length > 1) args = arguments; + if (args){ + result = {}; + for (var i = 0; i < args.length; i++) result[args[i]] = self.call(this, args[i]); + } else { + result = self.call(this, a); + } + return result; + }; +}; + +Function.prototype.extend = function(key, value){ + this[key] = value; +}.overloadSetter(); + +Function.prototype.implement = function(key, value){ + this.prototype[key] = value; +}.overloadSetter(); + +// From + +var slice = Array.prototype.slice; + +Function.from = function(item){ + return (typeOf(item) == 'function') ? item : function(){ + return item; + }; +}; + +Array.from = function(item){ + if (item == null) return []; + return (Type.isEnumerable(item) && typeof item != 'string') ? (typeOf(item) == 'array') ? item : slice.call(item) : [item]; +}; + +Number.from = function(item){ + var number = parseFloat(item); + return isFinite(number) ? number : null; +}; + +String.from = function(item){ + return item + ''; +}; + +// hide, protect + +Function.implement({ + + hide: function(){ + this.$hidden = true; + return this; + }, + + protect: function(){ + this.$protected = true; + return this; + } + +}); + +// Type + +var Type = this.Type = function(name, object){ + if (name){ + var lower = name.toLowerCase(); + var typeCheck = function(item){ + return (typeOf(item) == lower); + }; + + Type['is' + name] = typeCheck; + if (object != null){ + object.prototype.$family = (function(){ + return lower; + }).hide(); + //<1.2compat> + object.type = typeCheck; + // + } + } + + if (object == null) return null; + + object.extend(this); + object.$constructor = Type; + object.prototype.$constructor = object; + + return object; +}; + +var toString = Object.prototype.toString; + +Type.isEnumerable = function(item){ + return (item != null && typeof item.length == 'number' && toString.call(item) != '[object Function]' ); +}; + +var hooks = {}; + +var hooksOf = function(object){ + var type = typeOf(object.prototype); + return hooks[type] || (hooks[type] = []); +}; + +var implement = function(name, method){ + if (method && method.$hidden) return; + + var hooks = hooksOf(this); + + for (var i = 0; i < hooks.length; i++){ + var hook = hooks[i]; + if (typeOf(hook) == 'type') implement.call(hook, name, method); + else hook.call(this, name, method); + } + + var previous = this.prototype[name]; + if (previous == null || !previous.$protected) this.prototype[name] = method; + + if (this[name] == null && typeOf(method) == 'function') extend.call(this, name, function(item){ + return method.apply(item, slice.call(arguments, 1)); + }); +}; + +var extend = function(name, method){ + if (method && method.$hidden) return; + var previous = this[name]; + if (previous == null || !previous.$protected) this[name] = method; +}; + +Type.implement({ + + implement: implement.overloadSetter(), + + extend: extend.overloadSetter(), + + alias: function(name, existing){ + implement.call(this, name, this.prototype[existing]); + }.overloadSetter(), + + mirror: function(hook){ + hooksOf(this).push(hook); + return this; + } + +}); + +new Type('Type', Type); + +// Default Types + +var force = function(name, object, methods){ + var isType = (object != Object), + prototype = object.prototype; + + if (isType) object = new Type(name, object); + + for (var i = 0, l = methods.length; i < l; i++){ + var key = methods[i], + generic = object[key], + proto = prototype[key]; + + if (generic) generic.protect(); + + if (isType && proto){ + delete prototype[key]; + prototype[key] = proto.protect(); + } + } + + if (isType) object.implement(prototype); + + return force; +}; + +force('String', String, [ + 'charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf', 'match', 'quote', 'replace', 'search', + 'slice', 'split', 'substr', 'substring', 'toLowerCase', 'toUpperCase' +])('Array', Array, [ + 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift', 'concat', 'join', 'slice', + 'indexOf', 'lastIndexOf', 'filter', 'forEach', 'every', 'map', 'some', 'reduce', 'reduceRight' +])('Number', Number, [ + 'toExponential', 'toFixed', 'toLocaleString', 'toPrecision' +])('Function', Function, [ + 'apply', 'call', 'bind' +])('RegExp', RegExp, [ + 'exec', 'test' +])('Object', Object, [ + 'create', 'defineProperty', 'defineProperties', 'keys', + 'getPrototypeOf', 'getOwnPropertyDescriptor', 'getOwnPropertyNames', + 'preventExtensions', 'isExtensible', 'seal', 'isSealed', 'freeze', 'isFrozen' +])('Date', Date, ['now']); + +Object.extend = extend.overloadSetter(); + +Date.extend('now', function(){ + return +(new Date); +}); + +new Type('Boolean', Boolean); + +// fixes NaN returning as Number + +Number.prototype.$family = function(){ + return isFinite(this) ? 'number' : 'null'; +}.hide(); + +// Number.random + +Number.extend('random', function(min, max){ + return Math.floor(Math.random() * (max - min + 1) + min); +}); + +// forEach, each + +var hasOwnProperty = Object.prototype.hasOwnProperty; +Object.extend('forEach', function(object, fn, bind){ + for (var key in object){ + if (hasOwnProperty.call(object, key)) fn.call(bind, object[key], key, object); + } +}); + +Object.each = Object.forEach; + +Array.implement({ + + forEach: function(fn, bind){ + for (var i = 0, l = this.length; i < l; i++){ + if (i in this) fn.call(bind, this[i], i, this); + } + }, + + each: function(fn, bind){ + Array.forEach(this, fn, bind); + return this; + } + +}); + +// Array & Object cloning, Object merging and appending + +var cloneOf = function(item){ + switch (typeOf(item)){ + case 'array': return item.clone(); + case 'object': return Object.clone(item); + default: return item; + } +}; + +Array.implement('clone', function(){ + var i = this.length, clone = new Array(i); + while (i--) clone[i] = cloneOf(this[i]); + return clone; +}); + +var mergeOne = function(source, key, current){ + switch (typeOf(current)){ + case 'object': + if (typeOf(source[key]) == 'object') Object.merge(source[key], current); + else source[key] = Object.clone(current); + break; + case 'array': source[key] = current.clone(); break; + default: source[key] = current; + } + return source; +}; + +Object.extend({ + + merge: function(source, k, v){ + if (typeOf(k) == 'string') return mergeOne(source, k, v); + for (var i = 1, l = arguments.length; i < l; i++){ + var object = arguments[i]; + for (var key in object) mergeOne(source, key, object[key]); + } + return source; + }, + + clone: function(object){ + var clone = {}; + for (var key in object) clone[key] = cloneOf(object[key]); + return clone; + }, + + append: function(original){ + for (var i = 1, l = arguments.length; i < l; i++){ + var extended = arguments[i] || {}; + for (var key in extended) original[key] = extended[key]; + } + return original; + } + +}); + +// Object-less types + +['Object', 'WhiteSpace', 'TextNode', 'Collection', 'Arguments'].each(function(name){ + new Type(name); +}); + +// Unique ID + +var UID = Date.now(); + +String.extend('uniqueID', function(){ + return (UID++).toString(36); +}); + +//<1.2compat> + +var Hash = this.Hash = new Type('Hash', function(object){ + if (typeOf(object) == 'hash') object = Object.clone(object.getClean()); + for (var key in object) this[key] = object[key]; + return this; +}); + +Hash.implement({ + + forEach: function(fn, bind){ + Object.forEach(this, fn, bind); + }, + + getClean: function(){ + var clean = {}; + for (var key in this){ + if (this.hasOwnProperty(key)) clean[key] = this[key]; + } + return clean; + }, + + getLength: function(){ + var length = 0; + for (var key in this){ + if (this.hasOwnProperty(key)) length++; + } + return length; + } + +}); + +Hash.alias('each', 'forEach'); + +Object.type = Type.isObject; + +var Native = this.Native = function(properties){ + return new Type(properties.name, properties.initialize); +}; + +Native.type = Type.type; + +Native.implement = function(objects, methods){ + for (var i = 0; i < objects.length; i++) objects[i].implement(methods); + return Native; +}; + +var arrayType = Array.type; +Array.type = function(item){ + return instanceOf(item, Array) || arrayType(item); +}; + +this.$A = function(item){ + return Array.from(item).slice(); +}; + +this.$arguments = function(i){ + return function(){ + return arguments[i]; + }; +}; + +this.$chk = function(obj){ + return !!(obj || obj === 0); +}; + +this.$clear = function(timer){ + clearTimeout(timer); + clearInterval(timer); + return null; +}; + +this.$defined = function(obj){ + return (obj != null); +}; + +this.$each = function(iterable, fn, bind){ + var type = typeOf(iterable); + ((type == 'arguments' || type == 'collection' || type == 'array' || type == 'elements') ? Array : Object).each(iterable, fn, bind); +}; + +this.$empty = function(){}; + +this.$extend = function(original, extended){ + return Object.append(original, extended); +}; + +this.$H = function(object){ + return new Hash(object); +}; + +this.$merge = function(){ + var args = Array.slice(arguments); + args.unshift({}); + return Object.merge.apply(null, args); +}; + +this.$lambda = Function.from; +this.$mixin = Object.merge; +this.$random = Number.random; +this.$splat = Array.from; +this.$time = Date.now; + +this.$type = function(object){ + var type = typeOf(object); + if (type == 'elements') return 'array'; + return (type == 'null') ? false : type; +}; + +this.$unlink = function(object){ + switch (typeOf(object)){ + case 'object': return Object.clone(object); + case 'array': return Array.clone(object); + case 'hash': return new Hash(object); + default: return object; + } +}; + +// + +}).call(this); diff --git a/test/fixtures/package.yml b/test/fixtures/package.yml new file mode 100644 index 0000000..1ae8b48 --- /dev/null +++ b/test/fixtures/package.yml @@ -0,0 +1,11 @@ +name: "Core" + +license: "[MIT License](http://mootools.net/license.txt)" + +authors: "[MooTools Development Team](http://mootools.net/developers)" + +sources: + - "Source/Core.js" + - "Source/Array.js" + - "Source/Browser.js" + - "Source/Class.js" \ No newline at end of file diff --git a/test/packageTest.php b/test/packageTest.php new file mode 100644 index 0000000..826575e --- /dev/null +++ b/test/packageTest.php @@ -0,0 +1,30 @@ +parse_sources(array( + __DIR__ . '/fixtures/Source/Core.js', + __DIR__ . '/fixtures/Source/Browser.js' + )); + $sources = $package->get_sources(); + $this->assertEquals('Core', $sources[0]->get_name()); + $this->assertEquals('Browser', $sources[1]->get_name()); + + $package = new Package(__DIR__ . '/fixtures/package.yml'); + $sources = $package->get_sources(); + $this->assertEquals('Core', $sources[0]->get_name()); + $this->assertEquals('Class', end($sources)->get_name()); + $this->assertEquals(4, count($sources)); + } +} diff --git a/test/packagerTest.php b/test/packagerTest.php new file mode 100644 index 0000000..069ebe5 --- /dev/null +++ b/test/packagerTest.php @@ -0,0 +1,113 @@ +add_package(__DIR__ . '/fixtures/package.yml'); + $sources = $packager->get_sources(); + $this->assertEquals(4, count($sources)); + $this->assertEquals('Core', $sources[0]->get_name()); + } + + public function test_add_component() + { + $packager = new Packager(); + $source = new Source('test'); + + $this->assertEquals(-1, $packager->get_source_index($source)); + $index = $packager->add_component($source, 'test_add_component'); + $this->assertEquals($index, $packager->get_source_index($source)); + } + + public function test_get_required_for_source() + { + $packager = Packager::get_instance(); + $packager->add_package(__DIR__ . '/fixtures/package.yml'); + $sources = $packager->get_sources(); + + $class = end($sources); + + $required = $packager->get_required_for_source($class); + + $this->assertEquals(2, count($required)); + $this->assertEquals('Core', $required[0]->get_name()); + $this->assertEquals('Array', $required[1]->get_name()); + } + + public function test_get_source_by_name() + { + $packager = Packager::get_instance(); + $source = $packager->get_source_by_name('Class'); + $this->assertEquals('Class', $source->get_name()); + } + + public function test_build() + { + $packager = Packager::get_instance(); + $source = $packager->get_source_by_name('Class'); + $build = array( + file_get_contents(__DIR__ . '/fixtures/Source/Core.js'), + file_get_contents(__DIR__ . '/fixtures/Source/Array.js'), + file_get_contents(__DIR__ . '/fixtures/Source/Class.js') + ); + + $actual = $packager->build($source); + $this->assertEquals(implode('', $build), $actual); + + return $actual; + } + + /** + * @depends test_build + */ + public function test_strip_blocks($code) + { + $blocks = array('1.2compat'); + + $code = Packager::strip_blocks($code, $blocks); + + foreach ($blocks as $block){ + $this->assertNotRegExp("/<$block>/", $code, "Should not find '$block' opening block."); + $this->assertNotRegExp("/<\/$block>/", $code, "Should not find '$block' closing block."); + } + } + + public function test_remove_package() + { + $packager = Packager::get_instance(); + + $package = new Package(); + $package->set_name('Test Package'); + + $packager->add_package($package); + + $packages = $packager->get_packages(); + $this->assertEquals(2, count($packages)); + $this->assertEquals(end($packages), $package); + + $this->assertTrue($packager->remove_package($package)); + + $packages = $packager->get_packages(); + $this->assertEquals(1, count($packages)); + + $this->assertFalse($packager->remove_package('Random')); + } + + public function test_remove_package_and_components() + { + $packager = Packager::get_instance(); + + $this->assertTrue($packager->remove_package('Core')); + + $sources = $packager->get_sources(); + $this->assertEquals(0, count($sources)); + + $this->assertFalse(!!$packager->get_source_by_name('Core')); + } +} + diff --git a/test/sourceTest.php b/test/sourceTest.php new file mode 100644 index 0000000..57ec73e --- /dev/null +++ b/test/sourceTest.php @@ -0,0 +1,78 @@ +parse($source_path); + + $this->assertEquals('Class', $source->get_name()); + $this->assertEquals(file_get_contents($source_path), $source->get_code()); + $this->assertEquals(array('Class'), $source->get_provides()); + $this->assertEquals(array('Core/Array'), $source->get_requires()); + } + + public function test_no_deps_build() + { + $source = new Source('test'); + + $code = '/* code */'; + $source->set_code($code); + + $this->assertEquals($code, $source->build()); + } + + public function test_deps_build() + { + # todo(ibolmo): Cannot overwrite packages once they're defined. + $source = new Source('!Core'); + $source->provides('Type'); + $core_code = '/* Core */'; + $source->set_code($core_code); + + $source = new Source('Array'); + $source->requires('!Core/Type'); + $array_code = '/* Array */'; + $source->set_code($array_code); + + $this->assertEquals($core_code.$array_code, $source->build()); + } + + public function test_redundant_deps() + { + $source = new Source('Base'); + $base_code = '/* Base */'; + $source->provides('Base'); + $source->set_code($base_code); + + $source = new Source('Child'); + $source->provides('Child'); + $source->requires('Base'); + $child_code = '/* Child */'; + $source->set_code($child_code); + + $source = new Source('Sibling'); + $source->provides('Sibling'); + $source->requires('Base'); + $sibling_code = '/* Sibling */'; + $source->set_code($sibling_code); + + $source = new Source('Container'); + $source->requires(array('Child', 'Sibling')); + $container_code = '/* Container */'; + $source->set_code($container_code); + + $this->assertEquals($base_code.$child_code.$sibling_code.$container_code, $source->build()); + } +}