Repository: andrewdavey/vogue Branch: master Commit: 46e38b373c7a Files: 16 Total size: 22.5 KB Directory structure: gitextract_lf45wp7e/ ├── .gitignore ├── demo/ │ ├── index.htm │ ├── novogue.htm │ ├── styles/ │ │ ├── demo.css │ │ ├── print.css │ │ └── reset.css │ └── sub/ │ └── index.htm ├── license.txt ├── readme.md └── src/ ├── README.md ├── VogueClient.js ├── Watcher.js ├── client/ │ ├── about.htm │ └── vogue-client.js ├── package.json └── vogue.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.swp src/node_modules ================================================ FILE: demo/index.htm ================================================ Vogue - Demo Page

Vogue - Demo Page

This is some demo text for Vogue

The vogue client is already added to this page. Try editing the CSS files to see the page update.

================================================ FILE: demo/novogue.htm ================================================ Vogue - Demo Page

Vogue - Demo Page

This is some demo text for Vogue

Vogue has not been loaded into this page. Try using the favlet.

================================================ FILE: demo/styles/demo.css ================================================ body { background-color: #fff; color: #000; font-family: sans-serif; padding: 10px; } ================================================ FILE: demo/styles/print.css ================================================ body { background: #f0f; /* something nasty to notice if vogue switches on this style! */ color: blue; } ================================================ FILE: demo/styles/reset.css ================================================ * { padding: 0; margin: 0 } ================================================ FILE: demo/sub/index.htm ================================================ Vogue - Demo Page

Vogue - Demo Page

This is some demo text for Vogue

The vogue client is already added to this page. Try editing the CSS files to see the page update.

================================================ FILE: license.txt ================================================ Copyright (c) 2011 Andrew Davey (andrew@equin.co.uk) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: readme.md ================================================ # Vogue Vogue creates a real-time link between your web browser and your file system. When you save a CSS file, used by the HTML page in your browser, Vogue will make the browser reload the stylesheet. Only the stylesheet is reloaded, not the entire page, making it work even for very dynamic/ajax pages. Vogue is all javascript. It runs a server on [Node.js](http://nodejs.org/), which will watch the file system. The server accepts WebSocket connections from the client code (which uses [socket.io](http://socket.io/)). The client javascript can be loaded into a HTML page using a single script tag. ## Install using npm Make sure you have Node.JS and [npm](http://npmjs.org/) installed. Then run: npm install vogue -g ## Usage Run the Vogue server. vogue --port 8001 /path/to/website `--port` : The port used for Vogue's HTTP server. Optional, defaults to 8001. `--rewrite` : A rule in the form of "regexp:replacement" (e.g. "v[0-9]/(.*)$:files/\$1" ) to rewrite urls to filesystem paths. Submatches such as $1 will probably need to entered in your shell as \$1 to escape the $. Open http://localhost:8001/ to see instructions for loading the Vogue client into your web pages. ## Demo Vogue runs a separate HTTP server to the one running your website. To run the demo website, for example, do something like this first: cd demo python -m SimpleHTTPServer Then, from another terminal session, run Vogue: vogue demo Open http://localhost:8000 (or whatever the port used by your web server is) to view the demo index page. The demo page has the Vogue client javascript already included. So it will connect to the Vogue server and be watching the two CSS files used by the page. Try editing the CSS files in the `demo/styles` directory. Whenever you save, you will see the browser update the reflect the changes made. This is done without reloading the entire page. Copyright © 2011 Andrew Davey (andrew@equin.co.uk) ================================================ FILE: src/README.md ================================================ # Vogue Vogue creates a real-time link between your web browser and your file system. When you save a CSS file, used by the HTML page in your browser, Vogue will make the browser reload the stylesheet. Only the stylesheet is reloaded, not the entire page, making it work even for very dynamic/ajax pages. ## Usage Run the Vogue server. vogue --port 8001 /path/to/website `--port` : The port used for Vogue's HTTP server. Optional, defaults to 8001. `--rewrite` : A rule in the form of "regexp:replacement" (e.g. `v[0-9]/(.*)$:files/\$1` ) to rewrite urls to filesystem paths. Submatches such as $1 will probably need to entered in your shell as \$1 to escape the $. Open http://localhost:8001/ to see instructions for loading the Vogue client into your web pages. ================================================ FILE: src/VogueClient.js ================================================ var fs = require('fs'); exports.VogueClient = VogueClient; // Encapsulates a web socket client connection from a web browser. function VogueClient(clientSocket, watcher) { this.socket = clientSocket; this.watcher = watcher; this.watchedFiles = {}; clientSocket.on('watch', this.handleMessage.bind(this)); clientSocket.on('disconnect', this.disconnect.bind(this)); } // Parse an incoming message from the client and dispatch accordingly. VogueClient.prototype.handleMessage = function(data) { this.watchFile(data.href); }; VogueClient.prototype.watchFile = function(href) { var filename = this.watcher.getFilenameForHref(href); fs.stat(filename, function(err, stats) { if (err) { console.log('Could not read stats for ' + filename); return; } this.watchedFiles[filename] = { href: href, mtime: stats.mtime }; this.watcher.startWatching(filename); }.bind(this)); }; VogueClient.prototype.updateFile = function(filename) { var fileInfo = this.watchedFiles[filename]; if (fileInfo) { fs.stat(filename, function(err, stats) { if (err) { console.error('Could not read stats for file: ' + filename); return; } // Only send message to client if the file was modified // since we last saw it. if (fileInfo.mtime < stats.mtime) { this.socket.emit('update', { href: fileInfo.href }); fileInfo.mtime = stats.mtime; } }.bind(this)); } }; VogueClient.prototype.disconnect = function() { for (var filename in this.watchedFiles) { this.watcher.stopWatching(filename); } this.watcher.removeClient(this); }; ================================================ FILE: src/Watcher.js ================================================ var fs = require('fs') , path = require('path'); exports.Watcher = Watcher; function Watcher(webDirectory, rewrite) { this.webDirectory = webDirectory; // array of VogueClient objects this.clients = []; // filename -> number_of_clients_watching this.fileWatcherCount = {}; if (rewrite) { this.rewriteUrlToPath = createRewriter(rewrite); } function createRewriter(rewrite) { var parts = rewrite.split(':'); if (parts.length === 2) { var regex = new RegExp(parts[0]); var replacement = parts[1]; return function (str) { return str.replace(regex, replacement); } } else { throw new Error('Rewrite must be of the form "regex:replacement".'); } } } Watcher.prototype.addClient = function(client) { this.clients.push(client); }; Watcher.prototype.removeClient = function(client) { this.clients.splice(this.clients.indexOf(client), 1); }; Watcher.prototype.getFilenameForHref = function(href) { if (this.rewriteUrlToPath) { href = this.rewriteUrlToPath(href); } // Remove any querystring junk. // e.g. "foo/bar.css?abc=123" --> "foo/bar.css" href = href.split('?')[0]; var filename = path.join(this.webDirectory, href); return filename; }; Watcher.prototype.startWatching = function(filename) { console.log('Watching file: ' + filename); if (filename in this.fileWatcherCount) { // already watching this file, so just increment the client count. this.fileWatcherCount[filename]++; } else { fs.watchFile( filename, { persistent: true, interval: 50 }, fileChanged.bind(this) ); this.fileWatcherCount[filename] = 1; } function fileChanged() { console.log('File changed: ' + filename); this.clients.forEach(function(client) { client.updateFile(filename); }); } }; Watcher.prototype.stopWatching = function(filename) { if (!(filename in this.fileWatcherCount)) return; var watcherCount = --this.fileWatcherCount[filename]; if (watcherCount == 0) { delete this.fileWatcherCount[filename]; fs.unwatchFile(filename); console.log('Stopped watching file: ' + filename); } } ================================================ FILE: src/client/about.htm ================================================ Vogue

Vogue

Add this bookmarklet to your bookmarks. It will inject Vogue's scripts into the currently running page.

Inject Vogue

Or, add the following script into your HTML.

<script src="http://localhost:{port}/vogue-client.js" type="text/javascript"></script>

Note: This should only appear in development-time code. Remove before going into production.

================================================ FILE: src/client/vogue-client.js ================================================ // Vogue - Client // Copyright (c) 2011 Andrew Davey (andrew@equin.co.uk) (function () { var script, hop = Object.prototype.hasOwnProperty, head = document.getElementsByTagName("head")[0]; function vogue() { var stylesheets, socket = io.connect(script.rootUrl); /** * Watch for all available stylesheets. */ function watchAllStylesheets() { var href; for (href in stylesheets) { if (hop.call(stylesheets, href)) { socket.emit("watch", { href: href }); } } } /** * Reload a stylesheet. * * @param {String} href The URL of the stylesheet to be reloaded. */ function reloadStylesheet(href) { var newHref = stylesheets[href].href + (href.indexOf("?") >= 0 ? "&" : "?") + "_vogue_nocache=" + (new Date).getTime(), stylesheet; // Check if the appropriate DOM Node is there. if (!stylesheets[href].setAttribute) { // Create the link. stylesheet = document.createElement("link"); stylesheet.setAttribute("rel", "stylesheet"); stylesheet.setAttribute("href", newHref); head.appendChild(stylesheet); // Update the reference to the newly created link. stylesheets[href] = stylesheet; } else { // Update the href to the new URL. stylesheets[href].href = newHref; } } /** * Handle messages from socket.io, and load the appropriate stylesheet. * * @param message Socket.io message object. * @param message.href The url of the stylesheet to be loaded. */ function handleMessage(message) { reloadStylesheet(message.href); } /** * Fetch all the local stylesheets from the page. * * @returns {Object} The list of local stylesheets keyed by their base URL. */ function getLocalStylesheets() { /** * Checks if the stylesheet is local. * * @param {Object} link The link to check for. * @returns {Boolean} */ function isLocalStylesheet(link) { var href, i, isExternal = true; if (link.getAttribute("rel") !== "stylesheet") { return false; } href = link.href; for (i = 0; i < script.bases.length; i += 1) { if (href.indexOf(script.bases[i]) > -1) { isExternal = false; break; } } return !(isExternal && href.match(/^https?:/)); } /** * Checks if the stylesheet's media attribute is 'print' * * @param (Object) link The stylesheet element to check. * @returns (Boolean) */ function isPrintStylesheet(link) { return link.getAttribute("media") === "print"; } /** * Get the link's base URL. * * @param {String} href The URL to check. * @returns {String|Boolean} The base URL, or false if no matches found. */ function getBase(href) { var base, j; for (j = 0; j < script.bases.length; j += 1) { base = script.bases[j]; if (href.indexOf(base) > -1) { return href.substr(base.length); } } return false; } function getProperty(property) { return this[property]; } var stylesheets = {}, reImport = /@import\s+url\(["']?([^"'\)]+)["']?\)/g, links = document.getElementsByTagName("link"), link, href, matches, content, i, m; // Go through all the links in the page, looking for stylesheets. for (i = 0, m = links.length; i < m; i += 1) { link = links[i]; if (isPrintStylesheet(link)) continue; if (!isLocalStylesheet(link)) continue; // Link is local, get the base URL. href = getBase(link.href); if (href !== false) { stylesheets[href] = link; } } // Go through all the style tags, looking for @import tags. links = document.getElementsByTagName("style"); for (i = 0, m = links.length; i < m; i += 1) { if (isPrintStylesheet(links[i])) continue; content = links[i].text || links[i].textContent; while ((matches = reImport.exec(content))) { link = { rel: "stylesheet", href: matches[1], getAttribute: getProperty }; if (isLocalStylesheet(link)) { // Link is local, get the base URL. href = getBase(link.href); if (href !== false) { stylesheets[href] = link; } } } } return stylesheets; } stylesheets = getLocalStylesheets(); socket.on("connect", watchAllStylesheets); socket.on("update", handleMessage); } /** * Load a script into the page, and call a callback when it is loaded. * * @param {String} src The URL of the script to be loaded. * @param {Function} loadedCallback The function to be called when the script is loaded. */ function loadScript(src, loadedCallback) { var script = document.createElement("script"); script.setAttribute("type", "text/javascript"); script.setAttribute("src", src); // Call the callback when the script is loaded. script.onload = loadedCallback; script.onreadystatechange = function () { if (this.readyState === "complete" || this.readyState === "loaded") { loadedCallback(); } }; head.appendChild(script); } /** * Load scripts into the page, and call a callback when they are loaded. * * @param {Array} scripts The scripts to be loaded. * @param {Function} loadedCallback The function to be called when all the scripts have loaded. */ function loadScripts(scripts, loadedCallback) { var srcs = [], property, count, i, src, countDown = function () { count -= 1; if (!count) { loadedCallback(); } }; for (property in scripts) { if (!(property in window)) { srcs.push(scripts[property]); } } count = srcs.length; if (!count) { loadedCallback(); } for (i = 0; i < srcs.length; i += 1) { src = srcs[i]; loadScript(src, countDown); } } /** * Fetches the info for the vogue client. */ function getScriptInfo() { var bases = [ document.location.protocol + "//" + document.location.host ], scripts, src, rootUrl, baseMatch; if (typeof window.__vogue__ === "undefined") { scripts = document.getElementsByTagName("script"); for (var i=0; i < scripts.length; i++) { src = scripts[i].getAttribute("src"); if (src && src.slice(-15) === 'vogue-client.js') break; } rootUrl = src.match(/^https?\:\/\/(.*?)\//)[0]; // There is an optional base argument, that can be used. baseMatch = src.match(/\bbase=(.*)(&|$)/); if (baseMatch) { bases = bases.concat(baseMatch[1].split(",")); } return { rootUrl: rootUrl, bases: bases }; } else { window.__vogue__.bases = bases; return window.__vogue__; } } /** * Fetches the port from the URL. * * @param {String} url URL to get the port from * @returns {Number} The port number, or 80 if no port number found or is invalid. */ function getPort(url) { // URL may contain the port number after the second colon. // http://domain:1234/ var index = url.indexOf(":", 6); // skipping 6 characters to ignore first colon return index < 0 ? 80 : parseInt(url.substr(index + 1), 10); } script = getScriptInfo(); loadScripts({ io: script.rootUrl + "socket.io/socket.io.js" }, vogue); }()); ================================================ FILE: src/package.json ================================================ { "name": "vogue", "description": "Auto-reload stylesheets in web browser whenever the CSS files are saved.", "version": "0.5.0", "homepage": "http://github.com/andrewdavey/vogue", "repository": "http://github.com/andrewdavey/vogue.git", "author": "Andrew Davey (http://aboutcode.net/)", "directories": { "lib": "" }, "engines": { "node": "*" }, "bin": { "vogue": "./vogue.js" }, "dependencies": { "socket.io": ">=0.9.0", "parseopt": ">=1.0.0" } } ================================================ FILE: src/vogue.js ================================================ #!/usr/bin/env node /* vogue.js * A tool for web developers. Vogue watches for changes to CSS files and * informs the web browser using them to reload those stylesheets. * * Created by Andrew Davey ~ http://aboutcode.net * * Vogue runs on nodeJS and uses socket.io for real-time communication between * browser and server. */ var http = require('http') , fs = require('fs') , path = require('path') , url = require('url') , opt = require('parseopt') , io = require('socket.io'); var VogueClient = require('./VogueClient').VogueClient , Watcher = require('./Watcher').Watcher; var options = getOptions() , server = http.createServer(handleHttpRequest) , socket = io.listen(server) , watcher = new Watcher(options.webDirectory,options.rewrite); server.listen(options.port); socket.sockets.on('connection', function(clientSocket) { watcher.addClient(new VogueClient(clientSocket, watcher)); }); console.log('Watching directory: ' + options.webDirectory); console.log('Listening for clients: http://localhost:' + options.port + '/'); function handleHttpRequest(request, response) { var pathname = url.parse(request.url).pathname; if (pathname === '/') { sendAboutPage(response); } else if (pathname === '/vogue-client.js') { sendVogueClient(response); } } function sendAboutPage(response) { fs.readFile(__dirname + '/client/about.htm', function(e, fileData) { var html = fileData.toString(); html = html.replace(/\{port\}/g, options.port.toString()); response.writeHead(200, { 'Content-Type': 'text/html' }); response.write(html); response.end(); }); } function sendVogueClient(response) { fs.readFile(__dirname + '/client/vogue-client.js', function(e, fileData) { var script = fileData.toString(); response.writeHead(200, { 'Content-Type': 'text/javascript' }); response.write(script); response.end(); }); } function getOptions() { var data = createOptionParser().parse(); if (!data) process.exit(1); // Some kind of parsing error // The directory to watch is given as the first argument after the options. // So we'll put it into the options we return for simplicity. data.options.webDirectory = getDirectoryToWatch(data.arguments); return data.options; function createOptionParser() { var parser = new opt.OptionParser({ options: [ { name: ['--port', '-p'], type: 'int', help: 'Port to run Vogue server on', 'default': 8001 }, { name: ['--rewrite', '-r'], type: 'string', help: 'Expression of the form "regexp:replacement" rewrites a URL path into a file system path, relative to the website root directory. For example: --rewrite "v[0-9]/(.*)$:files/\\$1" would change "v1/demo.css" to "files/demo.css".', 'default': null }, { name: ['--help','-h','-?'], type: 'flag', help: 'Show this help message', onOption: function (value) { if (value) { parser.usage('First argument after options should be the path to the website\'s root directory. Otherwise the current directory is used.\ne.g. vogue -p 8001 ./myweb'); } // returning true cancels any further option parsing // and parser.parse() returns null return value; } } ] }); return parser; } function getDirectoryToWatch(arguments) { var dir; if (arguments.length > 0) { if (/^\//.test(arguments[0])) { dir = arguments[0]; } else { dir = path.join(process.cwd(), arguments[0]); } } else { dir = process.cwd(); } try { var stats = fs.statSync(dir); if (!stats.isDirectory()) { console.error('Path is not a directory: ' + dir); process.exit(1); } } catch (e) { console.error('Path not found: ' + dir); process.exit(1); } return dir; } }