Index: branches/jhs-require/chrome/chromeFiles/content/config.js
===================================================================
--- branches/jhs-require/chrome/chromeFiles/content/config.js	(revision 697)
+++ branches/jhs-require/chrome/chromeFiles/content/config.js	(working copy)
@@ -172,92 +172,72 @@
     configStream.close();
   },
 
-  parse: function(source, uri) {
+  parse: function parse(source, uri, script, headers) {
+    function resolveURL(url, baseurl) {
+      url = ioservice.newURI(url, null, baseurl);
+      return url.spec;
+    }
     var ioservice = Components.classes["@mozilla.org/network/io-service;1"]
                               .getService(Components.interfaces.nsIIOService);
 
-    var script = new Script(this);
-    script._downloadURL = uri.spec;
-    script._enabled = true;
+    headers = GM_parseScriptHeaders(source, {
+      // verbatim string, last value only:
+      name: 1,
+      namespace: 1,
+      description: 1,
 
-    // read one line at a time looking for start meta delimiter or EOF
-    var lines = source.match(/.+/g);
-    var lnIdx = 0;
-    var result = {};
-    var foundMeta = false;
+      // verbatim array of strings, all occurrences:
+      include: 0,
+      exclude: 0,
 
-    while ((result = lines[lnIdx++])) {
-      if (result.indexOf("// ==UserScript==") == 0) {
-        foundMeta = true;
-        break;
-      }
-    }
-
-    // gather up meta lines
-    if (foundMeta) {
-      // used for duplicate resource name detection
-      var previousResourceNames = {};
-
-      while ((result = lines[lnIdx++])) {
-        if (result.indexOf("// ==/UserScript==") == 0) {
-          break;
+      // derived data, all occurrences:
+      require: function require(url, priorRequires) {
+        url = resolveURL(url, uri);
+        if (url == uri.spec)
+          return null;
+        for (var i = 0; i < priorRequires.length; i++) {
+          var seen = priorRequires[i];
+          if (seen && seen.url == url)
+            return null; // already required; NOP
         }
-
-        var match = result.match(/\/\/ \@(\S+)\s+([^\n]+)/);
-        if (match != null) {
-          switch (match[1]) {
-            case "name":
-            case "namespace":
-            case "description":
-              script["_" + match[1]] = match[2];
-              break;
-            case "include":
-              script._includes.push(match[2]);
-              break;
-            case "exclude":
-              script._excludes.push(match[2]);
-              break;
-            case "require":
-              var reqUri = ioservice.newURI(match[2], null, uri);
-              var scriptRequire = new ScriptRequire(script);
-              scriptRequire._downloadURL = reqUri.spec;
-              script._requires.push(scriptRequire);
-              break;
-            case "resource":
-              var res = match[2].match(/(\S+)\s+(.*)/);
-              if (res === null) {
-                // NOTE: Unlocalized strings
-                throw new Error("Invalid syntax for @resource declaration '" +
-                                match[2] + "'. Resources are declared like: " +
-                                "@resource <name> <url>.");
-              }
-
-              var resName = res[1];
-              if (previousResourceNames[resName]) {
-                throw new Error("Duplicate resource name '" + resName + "' " +
-                                "detected. Each resource must have a unique " +
-                                "name.");
-              } else {
-                previousResourceNames[resName] = true;
-              }
-
-              var resUri = ioservice.newURI(res[2], null, uri);
-              var scriptResource = new ScriptResource(script);
-              scriptResource._name = resName;
-              scriptResource._downloadURL = resUri.spec;
-              script._resources.push(scriptResource);
-              break;
-          }
+        return new ScriptDependency(url);
+      },
+      resource: function resource(name_url, priorResources) {
+        var args = name_url.match(/(\S+)\s+(.*)/);
+        if (args === null) {
+          throw new Error("Invalid syntax for @resource declaration '" +
+                          name_url + "'. Resources are declared like: " +
+                          "@resource <name> <url>."); // TODO: i18n
         }
+        var name = args[1];
+        for (var i = 0; i < priorpriorResources.length; i++) {
+          if (priorpriorResources[i].name == name)
+            throw new Error("Duplicate resource name '" + resName + "' " +
+                            "detected. Each resource must have a unique " +
+                            "name."); // TODO: i18n
+        }
+        var url = args[2];
+        return new ScriptResource(name, resolveURL(url, uri));
       }
+    }, headers);
+
+    if (!script) {
+      script = this.script = new Script();
+      script.uri = uri;
+      script.enabled = true;
+      script.name = headers.name || parseScriptName(uri);
+      script.namespace = headers.namespace || uri.host;
+      script.description = headers.description || "";
     }
+    script.includes = headers.include || ["*"];
+    script.excludes = headers.exclude || [];
+    script.requires = (headers.require || []).filter(function(r) { return r; });
+    script.resources = headers.resource || [];
 
-    // if no meta info, default to reasonable values
-    if (script._name == null) script._name = parseScriptName(uri);
-    if (script._namespace == null) script._namespace = uri.host;
-    if (!script._description) script._description = "";
-    if (script._includes.length == 0) script._includes.push("*");
+    return headers;
+  },
 
+  parse: function(source, uri) {
     return script;
   },
 
Index: branches/jhs-require/chrome/chromeFiles/content/utils.js
===================================================================
--- branches/jhs-require/chrome/chromeFiles/content/utils.js	(revision 697)
+++ branches/jhs-require/chrome/chromeFiles/content/utils.js	(working copy)
@@ -448,3 +448,74 @@
     }
   }
 }
+
+/**
+ * Returns an associative array from header name (sans @ prefix) to value.
+ * Values are arrays, unless headerSpec[name] was 1, in which case the value
+ * is a string only (the value of the last header with that name).
+ *
+ * If, instead of 1, a callback function is provided, the return value of that
+ * callback becomes appended to the array instead. This callback is invoked
+ * with two arguments: the raw header, and the array with all prior callback
+ * results for this header (or the empty array).
+ *
+ * oldHeaders (optional) is used as the target object headers are appended to.
+ * This is so we can accumulate and parse headers as new @require'd files show
+ * up asynchronously.
+ */
+function GM_parseScriptHeaders(source, headerSpec, oldHeaders) {
+  var headerRe = /.*/;
+  if (headerSpec) {
+    var allHeaders = [];
+    for (var header in headerSpec)
+      allHeaders.push(header);
+    headerRe = new RegExp("^(" + allHeaders.join("|") + ")$");
+  }
+
+  // read one line at a time looking for start meta delimiter or EOF
+  var lines = source.match(/.+/g);
+  var lnIdx = 0;
+  var result;
+  var foundMeta = false;
+  var headers = oldHeaders || {};
+
+  while ((result = lines[lnIdx++])) {
+    if (0 == result.indexOf("// ==UserScript==")) {
+      GM_log("* found metadata");
+      foundMeta = true;
+      break;
+    }
+  }
+
+  if (!foundMeta) {
+    return headers;
+  }
+
+  while ((result = lines[lnIdx++])) {
+    if (0 == result.indexOf("// ==/UserScript==")) {
+      break;
+    }
+
+    var match = result.match(/\/\/ \@(\S+)\s+([^\n]+)/);
+    if (match == null) {
+      continue;
+    }
+
+    var name = match[1], value = match[2];
+    if (!name.match(headerRe)) {
+      continue;
+    }
+
+    if (headerSpec && headerSpec[name] == 1) { // only wanted the last value
+      headers[name] = value;
+    } else { // wanted an array of all values
+      if (!headers.hasOwnProperty(name)) {
+        headers[name] = [];
+      }
+      var callback = headerSpec && headerSpec[name] ||
+        function identityFunc(header) { return header; };
+      headers[name].push(callback(value, headers[name]));
+    }
+  }
+  return headers;
+};
