#!/usr/bin/perl # Run perldoc on this file for documentation. # For the benefit of HTML viewers: #
$|++; my %data; my %transient; my %externalized_functions; my %datatypes; my %locations; # Maps eval-numbers to attribute names sub meta::define_form { my ($namespace, $delegate) = @_; $datatypes{$namespace} = $delegate; *{"meta::${namespace}::implementation"} = $delegate; *{"meta::$namespace"} = sub { my ($name, $value, %options) = @_; chomp $value; $data{"${namespace}::$name"} = $value unless $options{no_binding}; $delegate->($name, $value) unless $options{no_delegate}}} sub meta::eval_in { my ($what, $where) = @_; # Obtain next eval-number and alias it to the designated location @locations{eval('__FILE__') =~ /\(eval (\d+)\)/} = ($where); my $result = eval $what; $@ =~ s/\(eval \d+\)/$where/ if $@; warn $@ if $@; $result} meta::define_form 'meta', sub { my ($name, $value) = @_; meta::eval_in($value, "meta::$name")}; meta::meta('configure', <<'__'); # A function to configure transients. Transients can be used to store any number of # different things, but one of the more common usages is type descriptors. sub meta::configure { my ($datatype, %options) = @_; $transient{$_}{$datatype} = $options{$_} for keys %options; } __ meta::meta('externalize', <<'__'); # Function externalization. Data types should call this method when defining a function # that has an external interface. sub meta::externalize { my ($name, $attribute, $implementation) = @_; $externalized_functions{$name} = $attribute; *{"::$name"} = $implementation || $attribute; } __ meta::meta('functor::editable', <<'__'); # An editable type. This creates a type whose default action is to open an editor # on whichever value is mentioned. This can be changed using different flags. sub meta::functor::editable { my ($typename, %options) = @_; meta::configure $typename, %options; meta::define_form $typename, sub { my ($name, $value) = @_; $options{on_bind} && &{$options{on_bind}}($name, $value); meta::externalize $options{prefix} . $name, "${typename}::$name", sub { my $attribute = "${typename}::$name"; my ($command, @new_value) = @_; return &{$options{default}}(retrieve($attribute)) if ref $options{default} eq 'CODE' and not defined $command; return edit($attribute) if $command eq 'edit' or $options{default} eq 'edit' and not defined $command; return associate($attribute, @new_value ? join(' ', @new_value) : join('', )) if $command eq '=' or $command eq 'import' or $options{default} eq 'import' and not defined $command; return retrieve($attribute)}}} __ meta::meta('type::alias', <<'__'); meta::configure 'alias', inherit => 0; meta::define_form 'alias', sub { my ($name, $value) = @_; meta::externalize $name, "alias::$name", sub { # Can't pre-tokenize because shell::tokenize doesn't exist until the library:: # namespace has been evaluated (which will be after alias::). shell::run(shell::tokenize($value), shell::tokenize(@_)); }; }; __ meta::meta('type::bootstrap', <<'__'); # Bootstrap attributes don't get executed. The reason for this is that because # they are serialized directly into the header of the file (and later duplicated # as regular data attributes), they will have already been executed when the # file is loaded. meta::configure 'bootstrap', extension => '.pl', inherit => 1; meta::define_form 'bootstrap', sub {}; __ meta::meta('type::cache', <<'__'); meta::configure 'cache', inherit => 0; meta::define_form 'cache', \&meta::bootstrap::implementation; __ meta::meta('type::cached_dependency', <<'__'); meta::configure 'cached_dependency', inherit => 0, extension => ''; meta::define_form 'cached_dependency', \&meta::bootstrap::implementation; __ meta::meta('type::configuration', <<'__'); meta::functor::editable 'configuration', inherit => 0, extension => '.conf', default => sub { # Any lines starting with #, with or without leading whitespace, are treated as comments. # Comments are not parsed in option text; that is, you could specify an option that contained # a # and the # and following text would be considered part of that option. my ($data) = @_; my @options = grep /:/o && ! /^\h*#/o && ! /^\h*$/o, split(/\v+/o, $data); s/^\h+//o for @options; my @key_values = map split(/\s*:\s*/o, $_, 2), @options; $key_values[$_ << 1] and $key_values[$_ << 1] =~ s/\s/_/go for 0 .. @key_values >> 1; $key_values[$_ << 1] and $key_values[$_ << 1] = lc $key_values[$_ << 1] for 0 .. @key_values >> 1; @key_values; }; __ meta::meta('type::data', 'meta::functor::editable \'data\', extension => \'\', inherit => 0, default => \'cat\';'); meta::meta('type::function', <<'__'); meta::configure 'function', extension => '.pl', inherit => 1; meta::define_form 'function', sub { my ($name, $value) = @_; meta::externalize $name, "function::$name", meta::eval_in("sub {\n$value\n}", "function::$name"); }; __ meta::meta('type::hook', <<'__'); meta::configure 'hook', extension => '.pl', inherit => 0; meta::define_form 'hook', sub { my ($name, $value) = @_; *{"hook::$name"} = meta::eval_in("sub {\n$value\n}", "hook::$name"); }; __ meta::meta('type::inc', <<'__'); meta::configure 'inc', inherit => 1, extension => '.pl'; meta::define_form 'inc', sub { use File::Path 'mkpath'; use File::Basename qw/basename dirname/; my ($name, $value) = @_; my $tmpdir = basename($0) . '-' . $$; my $filename = "/tmp/$tmpdir/$name"; push @INC, "/tmp/$tmpdir" unless grep /^\/tmp\/$tmpdir$/, @INC; mkpath(dirname($filename)); unless (-e $filename) { open my $fh, '>', $filename; print $fh $value; close $fh; } }; __ meta::meta('type::internal_function', <<'__'); meta::configure 'internal_function', extension => '.pl', inherit => 1; meta::define_form 'internal_function', sub { my ($name, $value) = @_; *{$name} = meta::eval_in("sub {\n$value\n}", "internal_function::$name"); }; __ meta::meta('type::js', 'meta::functor::editable \'js\', extension => \'.js\', inherit => 1;'); meta::meta('type::library', <<'__'); meta::configure 'library', extension => '.pl', inherit => 1; meta::define_form 'library', sub { my ($name, $value) = @_; meta::eval_in($value, "library::$name"); }; __ meta::meta('type::message_color', <<'__'); meta::configure 'message_color', extension => '', inherit => 1; meta::define_form 'message_color', sub { my ($name, $value) = @_; terminal::color($name, $value); }; __ meta::meta('type::meta', <<'__'); # This doesn't define a new type. It customizes the existing 'meta' type # defined in bootstrap::initialization. Note that horrible things will # happen if you redefine it using the editable functor. meta::configure 'meta', extension => '.pl', inherit => 1; __ meta::meta('type::parent', <<'__'); meta::define_form 'parent', \&meta::bootstrap::implementation; meta::configure 'parent', extension => '', inherit => 1; __ meta::meta('type::retriever', <<'__'); meta::configure 'retriever', extension => '.pl', inherit => 1; meta::define_form 'retriever', sub { my ($name, $value) = @_; $transient{retrievers}{$name} = meta::eval_in("sub {\n$value\n}", "retriever::$name"); }; __ meta::meta('type::sdoc', <<'__'); # A meta-type for other types. So retrieve('js::main') will work if you have # the attribute 'sdoc::js::main'. The filename will be main.js.sdoc. meta::functor::editable 'sdoc', inherit => 1, extension => sub { extension_for(attribute($_[0])) . '.sdoc'; }; __ meta::meta('type::state', <<'__'); # Allows temporary or long-term storage of states. Nothing particularly insightful # is done about compression, so storing alternative states will cause a large # increase in size. Also, states don't contain other states -- otherwise the size # increase would be exponential. # States are created with the save-state function. meta::configure 'state', inherit => 0, extension => '.pl'; meta::define_form 'state', \&meta::bootstrap::implementation; __ meta::alias('ec', 'edit sdoc::js::montenegro.client'); meta::alias('es', 'edit sdoc::js::montenegro.server'); meta::bootstrap('html', <<'__'); __ meta::bootstrap('initialization', <<'__'); #!/usr/bin/perl # Run perldoc on this file for documentation. # For the benefit of HTML viewers: #
$|++; my %data; my %transient; my %externalized_functions; my %datatypes; my %locations; # Maps eval-numbers to attribute names sub meta::define_form { my ($namespace, $delegate) = @_; $datatypes{$namespace} = $delegate; *{"meta::${namespace}::implementation"} = $delegate; *{"meta::$namespace"} = sub { my ($name, $value, %options) = @_; chomp $value; $data{"${namespace}::$name"} = $value unless $options{no_binding}; $delegate->($name, $value) unless $options{no_delegate}}} sub meta::eval_in { my ($what, $where) = @_; # Obtain next eval-number and alias it to the designated location @locations{eval('__FILE__') =~ /\(eval (\d+)\)/} = ($where); my $result = eval $what; $@ =~ s/\(eval \d+\)/$where/ if $@; warn $@ if $@; $result} meta::define_form 'meta', sub { my ($name, $value) = @_; meta::eval_in($value, "meta::$name")}; __ meta::bootstrap('perldoc', <<'__'); =head1 Self-modifying Perl script =head2 Original implementation by Spencer Tipping L The prototype for this script is licensed under the terms of the MIT source code license. However, this script in particular may be under different licensing terms. To find out how this script is licensed, please contact whoever sent it to you. Alternatively, you may run it with the 'license' argument if they have specified a license that way. You should not edit this file directly. For information about how it was constructed, go to L. For quick usage guidelines, run this script with the 'usage' argument. =cut __ meta::cache('parent-identification', <<'__'); /home/spencertipping/bin/configuration aa772900bb5b925cb84346bd72a4249d /home/spencertipping/bin/node-base da62d84a9e81832f089520c172982c1a /home/spencertipping/bin/object 99aeabc9ec7fe80b1b39f5e53dc7e49e /home/spencertipping/bin/repository 05bc3036c343fdb8aec5b0be12a9b19e /home/spencertipping/conjectures/perl-objects/sdoc a1e8480e579614c01dabeecf0f963bcc __ meta::cached_dependency('caterwaul.all.js', '(function (f){return f(f)})(function (self,undefined){var qw=function (x){return x.split(/\\s+/)},id=function (x){return x},se=function (x,f){return f&&f.call(x,x)||x},gensym=(function (n,m){return function (){return \'gensym_\'+n.toString(36)+\'_\'+( ++m).toString(36)}})( +new Date(),Math.random()*(1<<30)>>>0),bind=function (f,t){return f.binding===t?f:f.original?bind(f.original,t):merge(function (){return f.apply(t,arguments)},{original:f,binding:t})},map=function (f,xs){for (var i=0,ys=[],l=xs.length;imax?k.length:max);o._max_length=max;return o},has=function (o,p){return p&& !(p.length>o._max_length)&&p!==\'_max_length\'&&own.call(o,p)},own=Object.prototype.hasOwnProperty,_caterwaul=typeof caterwaul===\'undefined\'?undefined:caterwaul,syntax_node_inspect=function (x){return x?x.inspect():\'(<>)\'},syntax_node_tostring=function (x){return x?x.serialize?x.serialize():x.toString():\'\'},node_methods={_replace:function (n){return (n.l=this.l)&&(this.l.r=n),(n.r=this.r)&&(this.r.l=n),this},_append_to:function (n){return n&&n._append(this),this},_reparent:function (n){return this.p&&this.p[0]===this&&(this.p[0]=n),this},_fold_l:function (n){return this._append(this.l&&this.l._unlink(this))},_append:function (n){return (this[this.length++]=n)&&(n.p=this),this},_fold_r:function (n){return this._append(this.r&&this.r._unlink(this))},_sibling:function (n){return n.p=this.p,(this.r=n).l=this},_fold_lr:function (){return this._fold_l()._fold_r()},_wrap:function (n){return n.p=this._replace(n).p,this._reparent(n),delete this.l,delete this.r,this._append_to(n)},_fold_rr:function (){return this._fold_r()._fold_r()},_unlink:function (n){return this.l&&(this.l.r=this.r),this.r&&(this.r.l=this.l),delete this.l,delete this.r,this._reparent(n)},pop:function (){return --this.length,this},push:function (x){return this[this.length++]=x,this},id:function (){return this.id||(this.id=gensym())},each:function (f){for (var i=0,l=this.length;i=0; --i)n.push(ns[i])},this))},unflatten:function (){var right=has(parse_associates_right,this.data);return this.length<=2?this:se(new this.constructor(this.data),bind(function (n){if (right)for (var i=0,l=this.length-1;i=1; --i)n=n.push(i>1?new this.constructor(this.data):this[0]).push(this[i])[0]},this))},as:function (d){return this.data===d?this:new this.constructor(d).push(this)},is_string:function (){return /[\'"]/.test(this.data.charAt(0))},as_escaped_string:function (){return this.data.substr(1,this.data.length-2)},is_number:function (){return /^-?(0x|\\d|\\.\\d+)/.test(this.data)},as_number:function (){return Number(this.data)},is_boolean:function (){return this.data===\'true\'||this.data===\'false\'},as_boolean:function (){return this.data===\'true\'},is_regexp:function (){return /^\\/./.test(this.data)},as_escaped_regexp:function (){return this.data.substring(1,this.data.lastIndexOf(\'/\'))},has_grouped_block:function (){return has(parse_r_until_block,this.data)},is_block:function (){return has(parse_block,this.data)},is_blockless_keyword:function (){return has(parse_r_optional,this.data)},is_null_or_undefined:function (){return this.data===\'null\'||this.data===\'undefined\'},is_constant:function (){return this.is_number()||this.is_string()||this.is_boolean()||this.is_regexp()||this.is_null_or_undefined()},left_is_lvalue:function (){return /=$/.test(this.data)||/\\+\\+$/.test(this.data)||/--$/.test(this.data)},is_empty:function (){return !this.length},has_parameter_list:function (){return this.data===\'function\'||this.data===\'catch\'},has_lvalue_list:function (){return this.data===\'var\'||this.data===\'const\'},is_dereference:function (){return this.data===\'.\'||this.data===\'[]\'},is_invocation:function (){return this.data===\'()\'},is_contextualized_invocation:function (){return this.is_invocation()&&this[0]&&this[0].is_dereference()},is_invisible:function (){return has(parse_invisible,this.data)},is_binary_operator:function (){return has(parse_lr,this.data)},is_prefix_unary_operator:function (){return has(parse_r,this.data)},is_postfix_unary_operator:function (){return has(parse_l,this.data)},is_unary_operator:function (){return this.is_prefix_unary_operator()||this.is_postfix_unary_operator()},accepts:function (e){return parse_accepts[this.data]&&this.accepts[parse.data]===(e.data||e)},bindings:function (hash){var result=hash||{};this.reach(function (n){if (n.binds_a_value)result[n.data]=n.value});return result},match:function (pattern){return macro_try_match(pattern,this)},ends_with_block:function (){var block_index=parse_r_until_block[this.data],block=this[block_index];return this.data===\'{\'||has(parse_r_until_block,this.data)&&(this.data!==\'function\'||this.length===3)&&block&&block.ends_with_block()},toString:function (){return this.inspect()},inspect:function (){return (this.l?\'(left) <- \':\'\')+\'(\'+this.data+(this.length?\' \'+map(syntax_node_inspect,this).join(\' \'):\'\')+\')\'+(this.r?\' -> \'+this.r.inspect():\'\')},serialize:function (){var op=this.data,right=this.r?\'/* -> \'+this.r.serialize()+\' */\':\'\',space=/\\w/.test(op.charAt(op.length-1))?\' \':\'\',s=has(parse_invisible,op)?map(syntax_node_tostring,this).join(space):has(parse_invocation,op)?map(syntax_node_tostring,[this[0],op.charAt(0),this[1],op.charAt(1)]).join(space):has(parse_ternary,op)?map(syntax_node_tostring,[this[0],op,this[1],parse_group[op],this[2]]).join(space):has(parse_group,op)?op+map(syntax_node_tostring,this).join(space)+parse_group[op]:has(parse_lr,op)?this.length?map(syntax_node_tostring,this).join(space+op+space):op:has(parse_r,op)||has(parse_r_optional,op)?op.replace(/^u/,\' \')+space+(this[0]?this[0].serialize():\'\'):has(parse_r_until_block,op)?has(parse_accepts,op)&&this[1]&&this[2]&&parse_accepts[op]===this[2].data&& !this[1].ends_with_block()?op+space+map(syntax_node_tostring,[this[0],this[1],\';\',this[2]]).join(\'\'):op+space+map(syntax_node_tostring,this).join(\'\'):has(parse_l,op)?(this[0]?this[0].serialize():\'\')+space+op:op;return right?s+right:s}},ref=extend(function (value){if (value instanceof this.constructor){this.value=value.value;this.data=value.data}else {this.value=value;this.data=gensym()}},{length:0,binds_a_value:true},node_methods),syntax_node=extend(function (data){if (data instanceof this.constructor)this.data=data.data,this.length=0;else {this.data=data&&data.toString();this.length=0;for (var i=1,l=arguments.length,_;_=arguments[i],i> >>> < > <= >= instanceof in == != === !== & ^ | && || ? = += -= *= /= %= &= |= ^= <<= >>= >>>= : , \'+\'return throw case var const break continue void else u; ;\'),lex_table=function (s){for (var i=0,xs=[false];i<8; ++i)xs.push.apply(xs,xs);for (var i=0,l=s.length;i?:;.,\'),lex_eol=lex_table(\'\\n\\r\'),lex_regexp_suffix=lex_table(\'gims\'),lex_quote=lex_table(\'\\\'"/\'),lex_slash=\'/\'.charCodeAt(0),lex_star=\'*\'.charCodeAt(0),lex_back=\'\\\\\'.charCodeAt(0),lex_x=\'x\'.charCodeAt(0),lex_dot=\'.\'.charCodeAt(0),lex_zero=\'0\'.charCodeAt(0),lex_postfix_unary=hash(\'++ --\'),lex_ident=lex_table(\'$_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\'),parse_reduce_order=map(hash,[\'function\',\'( [ . [] ()\',\'new delete\',\'u++ u-- ++ -- typeof u~ u! u+ u-\',\'* / %\',\'+ -\',\'<< >> >>>\',\'< > <= >= instanceof in\',\'== != === !==\',\'&\',\'^\',\'|\',\'&&\',\'||\',\'case\',\'?\',\'= += -= *= /= %= &= |= ^= <<= >>= >>>=\',\':\',\',\',\'return throw break continue void\',\'var const\',\'if else try catch finally for switch with while do\',\';\']),parse_associates_right=hash(\'= += -= *= /= %= &= ^= |= <<= >>= >>>= ~ ! new typeof u+ u- -- ++ u-- u++ ? if else function try catch finally for switch case with while do\'),parse_inverse_order=(function (xs){for (var o={},i=0,l=xs.length;i> >>> < > <= >= instanceof in == != === !== & ^ | && || = += -= *= /= %= &= |= ^= <<= >>= >>>= , : ;\'),parse_r_until_block=annotate_keys({\'function\':2,\'if\':1,\'do\':1,\'catch\':1,\'try\':1,\'for\':1,\'while\':1,\'with\':1}),parse_accepts=annotate_keys({\'if\':\'else\',\'do\':\'while\',\'catch\':\'finally\',\'try\':\'catch\'}),parse_invocation=hash(\'[] ()\'),parse_r_optional=hash(\'return throw break continue else\'),parse_also_expression=hash(\'function\'),parse_r=hash(\'u+ u- u! u~ u++ u-- new typeof finally var const void delete\'),parse_block=hash(\'; {\'),parse_invisible=hash(\'i;\'),parse_l=hash(\'++ --\'),parse_group=annotate_keys({\'(\':\')\',\'[\':\']\',\'{\':\'}\',\'?\':\':\'}),parse_ambiguous_group=hash(\'[ (\'),parse_ternary=hash(\'?\'),parse_not_a_value=hash(\'function if for while catch\'),parse=function (input){var s=input.toString(),mark=0,c=0,re=true,esc=false,dot=false,exp=false,close=0,t=\'\',i=0,l=s.length,cs=function (i){return s.charCodeAt(i)},grouping_stack=[],gs_top=null,head=null,parent=null,indexes=map(function (){return []},parse_reduce_order),invocation_nodes=[],all_nodes=[],new_node=function (n){return all_nodes.push(n),n},push=function (n){return head?head._sibling(head=n):(head=n._append_to(parent)),new_node(n)};while ((mark=i)=0;j+=inc)if (has(parse_lr,data))node._fold_lr();else if (has(parse_ambiguous_group,data)&&node.l&&(node.l.data===\'.\'|| !(has(lex_op,node.l.data)||has(parse_not_a_value,node.l.data))))invocation_nodes.push(node.l._wrap(new_node(new syntax_node(data+parse_group[data]))).p._fold_r());else if (has(parse_l,data))node._fold_l();else if (has(parse_r,data))node._fold_r();else if (has(parse_ternary,data)){node._fold_lr();var temp=node[1];node[1]=node[0];node[0]=temp}else if (has(parse_r_until_block,data)&&node.r&&node.r.data!==\':\'){for (var count=0,limit=parse_r_until_block[data];count=0; --i)_.r&&_._wrap(new syntax_node(\'i;\')).p._fold_r();for (var i=0,l=invocation_nodes.length,_,child;_=invocation_nodes[i],i=0; --i)delete all_nodes[i].p;return head},macro_array_push=Array.prototype.push,macro_try_match=function (pattern,t){if (pattern.data===\'_\')return [t];if (pattern.data!==t.data||pattern.length!==t.length)return null;for (var i=0,l=pattern.length,wildcards=[],match=null;i=0&&(macro=macros[i]); --i)if ((match=macro_try_match(macro,n))&&(replacement=expanders[i].apply(context,match)))return replacement})},compile=function (tree,environment){var vars=[],values=[],bindings=merge({},environment||{},tree.bindings()),s=gensym();for (var k in bindings)if (has(bindings,k))vars.push(k),values.push(bindings[k]);var code=map(function (v){return v===\'this\'?\'\':\'var \'+v+\'=\'+s+\'.\'+v},vars).join(\';\')+\';return(\'+tree.serialize()+\')\';try {return (new Function(s,code)).call(bindings[\'this\'],bindings)}catch (e){throw new Error(\'Caught \'+e+\' while compiling \'+code)}},associator_for=function (f){return function (name,behavior,value){return f[name]=(f.behaviors[f.attributes[name]=behavior]||id).call(f,value),f}},shallow_copy=function (x){return x&&(x.constructor===Array?x.slice():x.clone?x.clone():merge({},x))},copy_of=function (f){var g=merge(function (){return g.init.apply(g,arguments)},{behaviors:shallow_copy(f.behaviors),attributes:{}});return se(g,function (g){(g.associate=associator_for(g))(\'behavior\',\'method\',function (name,definition){this.behaviors[name]=definition;return this.associate(name,\'method\',function (attribute,value){return this.associate(attribute,name,value)})}).behavior(\'method\',g.behaviors.method);for (var k in f.attributes)has(f.attributes,k)&&g.associate(k,f.attributes[k],f[k])})},replica=se(function (){return copy_of({behaviors:{method:function (v){return bind(v,this)}}}).behavior(\'field\').behavior(\'shallow\',shallow_copy)},function (f){f.init=f}),configurable=function (f){return f.shallow(\'configurations\',{}).shallow(\'has\',{}).method(\'configuration\',function (name,f){this.configurations[name]=f;return this}).method(\'namespace\',function (s){return this[s]||this.shallow(s,{})[s]}).method(\'clone\',function (){return arguments.length?this.clone().configure.apply(null,arguments):copy_of(this)}).method(\'configure\',function (){for (var i=0,l=arguments.length,_;_=arguments[i],i=0&&l>>3,es=l>=0&&l&7,_i_=0;_i_1?x:this[0],xi=2-arguments.length][opt.unroll[i,this.l-xi][x=f.call(this,x,this[i+xi],i+xi)],x,when[this.l>=xi]],_.foldr(f,x)=l[x=arguments.length>1?x:this[this.l-1],xi=3-arguments.length,l=this.l][opt.unroll[i,l-(xi-1)][x=f.call(this,this[l-(i+xi)],x,l-(i+xi))],x,when[l>=xi-1]]]}).tconfiguration(\'std opt\',\'seq.finite.zip\',function (){this.configure(\'seq.finite.traversal\').seq.finite/se[_.prototype.zip()=l[as=new seq([this].concat(slice.call(arguments))),options={f:fn_[new seq(arguments)],outer:false}][caterwaul.util.merge(options,as.pop()),when[as[as.size()-1].constructor===Object],l[l=as.map(fn[x][x.size?x.size():x.length]).foldl(options.outer?fn[x,y][Math.max(x,y)]:fn[x,y][Math.min(x,y)]),f=options.f] in new this.constructor()/se[opt.unroll[i,l][_.push(f.apply({i:i},as.map(fn[x][x[i]]).slice()))]]],where[seq=_,slice=Array.prototype.slice]]}).tconfiguration(\'std opt continuation\',\'seq.finite.quantification\',function (){this.configure(\'seq.finite.core\').seq.finite.prototype/se[_.exists(f)=call/cc[fb[cc][opt.unroll[i,this.l][f.call(this,this[i],i)/re[_&&cc(_)]],false]],_.forall(f)= !this.exists(fn_[ !f.apply(this,arguments)])]}).tconfiguration(\'std\',\'seq.infinite.core\',function (){this.configure(\'seq.core\').seq.infinite=fn_[null]/se[_.prototype=new this.seq.core()/se[_.constructor=ctor],where[ctor=_]]/se[_.def(name,ctor,h,t)=i[name]=ctor/se[_.prototype=new i()/se[_.h=h,_.t=t,_.constructor=ctor]],where[i=_],_.def(\'cons\',fn[h,t][this._h=h,this._t=t],fn_[this._h],fn_[this._t]),_.def(\'k\',fn[x][this._x=x],fn_[this._x],fn_[this])]}).tconfiguration(\'std\',\'seq.infinite.y\',function (){this.configure(\'seq.infinite.core\').seq.infinite.def(\'y\',fc[f,x][this._f=f,this._x=x],fn_[this._x],fn_[new this.constructor(this._f,this._f(this._x))])}).tconfiguration(\'std continuation\',\'seq.infinite.transform\',function (){this.configure(\'seq.infinite.core\').seq.infinite/se[_.prototype.map(f)=new _.map(f,this),_.def(\'map\',fc[f,xs][this._f=f,this._xs=xs],fn_[this._f(this._xs.h())],fn_[new this.constructor(this._f,this._xs.t())]),_.prototype.filter(f)=new _.filter(f,this),_.def(\'filter\',fc[f,xs][this._f=f,this._xs=l*[next(s)(cc)=f(s.h())?cc(s):call/tail[next(s.t())(cc)]] in call/cc[next(xs)]],fn_[this._xs.h()],fn_[new this.constructor(this._f,this._xs.t())])]}).tconfiguration(\'std continuation\',\'seq.infinite.traversal\',function (){l[finite=this.configure(\'seq.finite.core seq.finite.mutability\').seq.finite] in this.configure(\'seq.infinite.core\').seq.infinite.prototype/se[_.drop(f)=l*[next(s)(cc)=f(s.h())?call/tail[next(s.t())(cc)]:cc(s)] in call/cc[next(this)],_.take(f)=l*[xs=new finite(),next(s)(cc)=l[h=s.h()][f(h)?(xs.push(h),call/tail[next(s.t())(cc)]):cc(xs)]] in call/cc[next(this)]]}).tconfiguration(\'std opt\',\'seq.numeric\',function (){this.configure(\'seq.infinite.core seq.infinite.y seq.finite.core\').seq/se[_.naturals_from(x)=new _.infinite.y(fn[n][n+1],x),_.naturals=_.naturals_from(0),_.n(l,u,s)=l[lower=arguments.length>1?l:0,upper=arguments.length>1?u:l][l[step=Math.abs(s||1)*(lower0])]]}).tconfiguration(\'std opt continuation\',\'seq.dsl\',function (){this.configure(\'seq.core seq.infinite.y seq.finite.core seq.finite.zip seq.finite.traversal seq.finite.mutability\').seq.dsl=caterwaul.global().clone()/se[_.prefix_substitute(tree,prefix)=tree.rmap(fn[n][new n.constructor(\'#{prefix}#{n.data.substring(1)}\'),when[n.data.charAt(0)===\'_\']]),_.define_functional(op,expansion,xs)=trees_for(op).map(fn[t,i][_.macro(t,fn[l,v,r][expansion.replace({_x:_.macroexpand(l),_y:i>=8?v:qs[fn[xs][y]].replace({fn:i&2?qs[fb]:qs[fn],xs:_.prefix_substitute(xs,i&1?v.data:\'_\'),y:(i&4?_.macroexpand:fn[x][x])(r||v)})})])]),_.define_functional/se[_(\'%\',qs[_x.filter(_y)],qs[_,_i]),_(\'*\',qs[_x.map(_y)],qs[_,_i]),_(\'/\',qs[_x.foldl(_y)],qs[_,_0,_i]),_(\'%!\',qs[_x.filter(c(_y))].replace({c:not}),qs[_,_i]),_(\'*!\',qs[_x.each(_y)],qs[_,_i]),_(\'/!\',qs[_x.foldr(_y)],qs[_,_0,_i]),_(\'&\',qs[_x.forall(_y)],qs[_,_i]),_(\'|\',qs[_x.exists(_y)],qs[_,_i]),_(\'-\',qs[_x.flat_map(_y)],qs[_,_i]),_(\'>>\',qs[_x.drop(_y)],qs[_]),_(\'<<\',qs[_x.take(_y)],qs[_]),_(\'>>>\',qs[new caterwaul.seq.infinite.y(_y,_x)],qs[_])],seq(qw(\'> < >= <= == !=\')).each(fn[op][_.macro(qs[_+_].clone()/se[_.data=op],rxy(qs[qg[_x].size()+qg[_y].size()].clone()/se[_.data=op]))]),l[e(x)=_.macroexpand(x)] in _.macro/se[_(qs[_&&_],rxy(qse[qg[l[xp=_x][xp&&xp.size()?_y:xp]]])),_(qs[_||_],rxy(qse[qg[l[xp=_x][xp&&xp.size()?xp:_y]]])),_(qs[_===_],rxy(qs[qg[l[xp=_x,yp=_y][xp===yp||xp.size()===yp.size()&&xp.zip(yp).forall(fn[p][p[0]===p[1]])]]])),_(qs[_!==_],rxy(qs[qg[l[xp=_x,yp=_y][xp!==yp&&(xp.size()!==yp.size()||xp.zip(yp).exists(fn[p][p[0]!==p[1]]))]]])),_(qs[_^_],rxy(qs[_x.zip(_y)])),_(qs[_+_],rxy(qs[_x.concat(_y)])),_(qs[ !_],rxy(qs[_x.object()])),_(qs[_,_],rxy(qs[_x,_y])),_(qs[ ~_],rxy(qs[qg[new caterwaul.seq.finite(_x)]])),_(qs[_?_:_],fn[x,y,z][qs[x?y:z].replace({x:e(x),y:e(y),z:e(z)})]),l[rx(t)(x,y)=t.replace({_x:e(x),_y:y})][_(qs[_(_)],rx(qs[_x(_y)])),_(qs[_[_]],rx(qs[_x[_y]])),_(qs[_._],rx(qs[_x._y])),_(qs[_].as(\'(\'),rx(qs[qg[_x]]))],_(qs[ +_],fn[x][x]),l[rx(t)(x)=t.replace({x:x})][_(qs[N],fn_[qs[caterwaul.seq.naturals]]),_(qs[N[_]],rx(qs[caterwaul.seq.naturals_from(x)])),_(qs[n[_]],rx(qs[caterwaul.seq.n(x)]))],seq(qw(\'sk sv sp\')).zip(qw(\'keys values pairs\')).each(fb[p][_(qs[p[_]].replace({p:p[0]}),fn[x][qs[caterwaul.seq.finite.r(x)].replace({r:p[1],x:x})])])],this.rmacro(qs[seq[_]],_.macroexpand),where*[rxy(tree)(x,y)=tree.replace({_x:_.macroexpand(x),_y:y&&_.macroexpand(y)}),seq=fb[xs][new this.seq.finite(xs)],prepend(operator)(x)=qs[ -x].replace({x:x})/se[_.data=operator],tree_forms=l*[base=seq([qs[[_]],qs[_[_]]]),mod(fs,op)=fs.concat(fs.map(prepend(op)))] in mod(mod(base,\'u-\'),\'u~\').concat(seq([qs[ +_]])),template(op)(t)=qs[_+x].replace({x:t})/se[_.data=op],qw=caterwaul.util.qw,not=qs[qg[fn[f][fn_[ !f.apply(this,arguments)]]]],trees_for(op)=tree_forms/re[op.charAt(op.length-1)===\'!\'?_.map(prepend(\'u!\')):_]/re[_.map(template(op.replace(/!$/,\'\')))]]]}).configuration(\'seq\',function (){this.configure(\'seq.core seq.finite.core seq.finite.object seq.finite.mutability seq.finite.traversal seq.finite.zip seq.finite.quantification \'+\'seq.finite.serialization seq.infinite.core seq.infinite.y seq.infinite.transform seq.infinite.traversal \'+\'seq.numeric seq.dsl\')});caterwaul.tconfiguration(\'std seq\',\'heap\',function (){this.heap(less)=fc_[null]/se.c[c.prototype=new caterwaul.seq.finite()/se[_.constructor=c]/se[_.insert(x)=this.push(x).heapify_up(this.size()-1),_.root()=this[0],_.rroot()=this[0]/se[this.pop()/se[this[0]=_,this.heapify_down(0),when[this.size()]]],_.swap(i,j)=this/se[_[j]=_[i],_[i]=temp,where[temp=_[j]]],_.heapify_up(i)=this/se[_.swap(i,p).heapify_up(p),when[less.call(_,_[i],_[p])],where[p=i>>1]],_.heapify_down(i)=this/se[_.swap(lr,i).heapify_down(lr),unless[lr===i],where*[s=_.size(),r=i+1<<1,l=r-1,ll=ll&&split_lengths(x,st.input,st.i,l,_)/re[st.accept(st.i+_,x.exec(st.input.substr(st.i,_)))]]]:x.constructor===Function?fn[st][x.call(st,st.input,st.i)/re[_&&st.accept(st.i+_,st.input.substr(st.i,_))]]:l[index=index_entries(seq[sk[x]])] in fn[st][check_index(index,st.input,st.i)/re[_&&st.accept(st.i+_.length,x[_])]],where*[check_index(i,s,p)=seq[i|[_[\'@#{s}\']&&s,where[s=s.substr(p,_.length)]]],index_entries(xs)=l*[xsp=seq[ ~xs],ls=seq[sk[seq[ !(xsp*[[_.length,true]])]]*[Number(_)]]] in seq[ ~ls.slice().sort(fn[x,y][y-x])* ~l[ !(xsp%[_.length===l]*[[\'@#{_}\',true]]+[[\'length\',l]])]],add_absolute_anchors_to(x)=l[parts=/^\\/(.*)\\/(\\w*)$/.exec(x.toString())] in new RegExp(\'^#{parts[1]}$\',parts[2]),fail_length(re,s,p,l)=re.test(s.substr(p,l))?p+(l<<1)<=s.length?fail_length(re,s,p,l<<1):l<<1:l,split_lengths(re,s,p,l,u)=l*[b(l,u)=l+1>1))/re.m[re.test(s.substr(p,m))?b(m,u):b(l,m)]:l] in b(l,u)]])}).tconfiguration(\'std opt seq continuation\',\'parser.seq\',function (){this.configure(\'parser.core\').parser.defparser(\'seq\',fn_[l[as=arguments] in fn[state][call/cc[fn[cc][opt.unroll[i,as.length][(state=as[i](state))?result.push(state.result):cc(false)],state.accept(state.i,result)]],where[result=[]]]])}).tconfiguration(\'std seq\',\'parser.alt\',function (){this.configure(\'parser.core\').parser.defparser(\'alt\',fn_[l[as=seq[ ~arguments]] in fn[state][seq[as|[_(state)]]]])}).tconfiguration(\'std opt seq continuation\',\'parser.times\',function (){this.configure(\'parser.core\').parser.defparser(\'times\',fn[p,lower,upper][fn[state][call/cc[fn[cc][opt.unroll[i,lower][ ++count,(state=p(state))?result.push(state.result):cc(false)],true]]&&call/cc[l*[loop(cc)=( !upper||count++>_],b(\'bind\')),_(qs[[_]],u(\'opt\')),_(qs[_].as(\'(\'),fn[x][e(x).as(\'(\')]),_(qs[_[_]],fn[x,l][qs[times(_x,_l)].replace({_x:e(x),_l:l})]),_(qs[_[_,_]],fn[x,l,u][qs[times(_x,_l,_u)].replace({_x:e(x),_l:l,_u:u})]),where*[e=dsl.macroexpand,fb(op,name)(x,y)=qs[_name(_x,_y)].replace({_name:name,_x:x.flatten(op).map(e)/se[_.data=\',\'],_y:e(y)}),b(name)(x,y)=qs[_name(_x,_y)].replace({_name:name,_x:e(x),_y:y}),u(name)(x)=qs[_name(_x)].replace({_name:name,_x:e(x)})]]]}).configuration(\'parser\',function (){this.configure(\'parser.core parser.c parser.seq parser.alt parser.times parser.opt parser.match parser.bind parser.dsl\')});'); meta::configuration('dependencies', <<'__'); # Named dependencies: caterwaul.all.js: http://spencertipping.com/caterwaul/caterwaul.all.min.js #montenegro.server.js: http://spencertipping.com/montenegro/montenegro.server.js __ meta::data('default-action', 'shell'); meta::data('libraries', <<'__'); # URLs of libraries to be downloaded into the lib/ directory. http://spencertipping.com/caterwaul/caterwaul.all.js http://spencertipping.com/montenegro/montenegro.server.js __ meta::data('license', <<'__'); MIT License Copyright (c) 2010 Spencer Tipping 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. __ meta::data('main', 'server.js'); meta::data('name', 'node-base'); meta::data('permanent-identity', '6f5918a987b5a6cfadd564963a5e2af2'); meta::data('quiet', '1'); meta::data('watching', '1'); meta::function('alias', <<'__'); my ($name, @stuff) = @_; return ls('-a', '^alias::') unless defined $name; @stuff ? around_hook('alias', @_, sub {associate("alias::$name", join ' ', @stuff)}) : retrieve("alias::$name") || "Undefined alias $name"; __ meta::function('cat', 'join "\\n", retrieve(@_);'); meta::function('cc', <<'__'); # Stashes a quick one-line continuation. (Used to remind me what I was doing.) @_ ? associate('data::current-continuation', hook('set-cc', join(' ', @_))) : retrieve('data::current-continuation'); __ meta::function('ccc', 'rm(\'data::current-continuation\');'); meta::function('child', <<'__'); around_hook('child', @_, sub { my ($child_name) = @_; clone($child_name); enable(); qx($child_name update-from $0 -n); disable()}); __ meta::function('clone', <<'__'); for (grep length, @_) { around_hook('clone', $_, sub { hypothetically(sub { rm('data::permanent-identity'); file::write($_, serialize(), noclobber => 1); chmod(0700, $_)})})} __ meta::function('cp', <<'__'); my $from = shift @_; my $value = retrieve($from); associate($_, $value) for @_; __ meta::function('create', <<'__'); my ($name, $value) = @_; around_hook('create', $name, $value, sub { return edit($name) if exists $data{$name}; associate($name, defined $value ? $value : ''); edit($name) unless defined $value}); __ meta::function('current-state', 'serialize(\'-pS\');'); meta::function('disable', 'hook(\'disable\', chmod_self(sub {$_[0] & 0666}));'); meta::function('dupdate', <<'__'); # Update the repository based on the dependencies it lists. use LWP::Simple (); rm(grep /^cached_dependency::/, keys %data); my %dependencies = dependencies(); for (keys %dependencies) { terminal::info("Retrieving $dependencies{$_} as $_"); associate("cached_dependency::$_", LWP::Simple::get($dependencies{$_}))} __ meta::function('edit', <<'__'); my ($name, %options) = @_; my $extension = extension_for($name); die "$name is virtual or does not exist" unless exists $data{$name}; die "$name is inherited; use 'edit $name -f' to edit anyway" unless is($name, '-u') || is($name, '-d') || exists $options{'-f'}; around_hook('edit', @_, sub { associate($name, invoke_editor_on($data{$name} || "# Attribute $name", %options, attribute => $name, extension => $extension), execute => $name !~ /^bootstrap::/)}); save() unless $data{'data::edit::no-save'}; ''; __ meta::function('enable', 'hook(\'enable\', chmod_self(sub {$_[0] | $_[0] >> 2}));'); meta::function('export', <<'__'); # Exports data into a text file. # export attr1 attr2 attr3 ... file.txt my $name = pop @_; @_ or die 'Expected filename'; file::write($name, join "\n", retrieve(@_)); __ meta::function('extern', '&{$_[0]}(retrieve(@_[1 .. $#_]));'); meta::function('grep', <<'__'); # Looks through attributes for a pattern. Usage is grep pattern [options], where # [options] is the format as provided to select_keys. my ($pattern, @args) = @_; my ($options, @criteria) = separate_options(@args); my @attributes = select_keys(%$options, '--criteria' => join('|', @criteria)); $pattern = qr/$pattern/; my @m_attributes; my @m_line_numbers; my @m_lines; for my $k (@attributes) { next unless length $k; my @lines = split /\n/, retrieve($k); for (0 .. $#lines) { next unless $lines[$_] =~ $pattern; push @m_attributes, $k; push @m_line_numbers, $_ + 1; push @m_lines, '' . ($lines[$_] // '')}} unless ($$options{'-C'}) { s/($pattern)/\033[1;31m\1\033[0;0m/g for @m_lines; s/^/\033[1;34m/o for @m_attributes; s/^/\033[1;32m/o && s/$/\033[0;0m/o for @m_line_numbers} table_display([@m_attributes], [@m_line_numbers], [@m_lines]); __ meta::function('hash', 'fast_hash(@_);'); meta::function('hook', <<'__'); my ($hook, @args) = @_; $transient{active_hooks}{$hook} = 1; dangerous('', sub {&$_(@args)}) for grep /^hook::${hook}::/, sort keys %data; @args; __ meta::function('hooks', 'join "\\n", sort keys %{$transient{active_hooks}};'); meta::function('identity', 'retrieve(\'data::permanent-identity\') || associate(\'data::permanent-identity\', fast_hash(rand() . name() . serialize()));'); meta::function('import', <<'__'); my $name = pop @_; associate($name, @_ ? join('', map(file::read($_), @_)) : join('', )); __ meta::function('initial-state', '$transient{initial};'); meta::function('is', <<'__'); my ($attribute, @criteria) = @_; my ($options, @stuff) = separate_options(@criteria); attribute_is($attribute, %$options); __ meta::function('load-state', <<'__'); around_hook('load-state', @_, sub { my ($state_name) = @_; my $state = retrieve("state::$state_name"); terminal::state('saving current state into _...'); &{'save-state'}('_'); delete $data{$_} for grep ! /^state::/, keys %data; %externalized_functions = (); terminal::state("restoring state $state_name..."); meta::eval_in($state, "state::$state_name"); terminal::error(hook('load-state-failed', $@)) if $@; reload(); verify()}); __ meta::function('loc', <<'__'); # Counts SLOC, whitespace, and total LOC in the codebase. hook('before-loc', @_); my $criteria = join '|', @_; my @attributes = grep s/^sdoc:://, select_keys('--criteria' => $criteria); my $tcomments = 0; my $twhitespace = 0; my $tsource = 0; my $line = sub { my ($source, $whitespace, $comments, $name) = @_; $source ||= 1; # Prevent divide-by-zero errors sprintf "%5d total, %4d SLOC, %5d[%4d%%] whitespace, %5d[%4d%%] comment [%s]", $source + $whitespace + $comments, $source, $whitespace, int($whitespace / $source * 100), $comments, int($comments / $source * 100), $name}; my $loc = sub { my @lines = map split(/\n/, $_), retrieve($_[0]); $tcomments += (my $comments = grep /^\s*\/\// || /^\s*#/, @lines); $twhitespace += (my $whitespace = grep /^\s*$/, @lines); $tsource += (my $source = @lines - $comments - $whitespace); &$line($source, $whitespace, $comments, $_[0])}; terminal::info(map &$loc($_), @attributes); terminal::info(&$line($tsource, $twhitespace, $tcomments, 'total')); hook('after-loc', @_); __ meta::function('lock', 'hook(\'lock\', chmod_self(sub {$_[0] & 0555}));'); meta::function('ls', <<'__'); my ($options, @criteria) = separate_options(@_); my ($external, $shadows, $sizes, $flags, $long, $hashes, $parent_hashes) = @$options{qw(-e -s -z -f -l -h -p)}; $sizes = $flags = $hashes = $parent_hashes = 1 if $long; return table_display([grep ! exists $data{$externalized_functions{$_}}, sort keys %externalized_functions]) if $shadows; my $criteria = join('|', @criteria); my @definitions = select_keys('--criteria' => $criteria, %$options); my %inverses = map {$externalized_functions{$_} => $_} keys %externalized_functions; my @externals = map $inverses{$_}, grep length, @definitions; my @internals = grep length $inverses{$_}, @definitions; my @sizes = map sprintf('%6d %6d', length(serialize_single($_)), length(retrieve($_))), @{$external ? \@internals : \@definitions} if $sizes; my @flags = map {my $k = $_; join '', map(is($k, "-$_") ? $_ : '-', qw(d i m u))} @definitions if $flags; my @hashes = map fast_hash(retrieve($_)), @definitions if $hashes; my %inherited = parent_attributes(grep /^parent::/o, keys %data) if $parent_hashes; my @parent_hashes = map $inherited{$_} || '-', @definitions if $parent_hashes; join "\n", map strip($_), split /\n/, table_display($external ? [grep length, @externals] : [@definitions], $sizes ? ([@sizes]) : (), $flags ? ([@flags]) : (), $hashes ? ([@hashes]) : (), $parent_hashes ? ([@parent_hashes]) : ()); __ meta::function('minify', 'node([\'cached_dependency::caterwaul.all.js\', \'js::minify\'], $_[0]);'); meta::function('mv', <<'__'); my ($from, $to) = @_; die "'$from' does not exist" unless exists $data{$from}; associate($to, retrieve($from)); rm($from); __ meta::function('name', <<'__'); my $name = $0; $name =~ s/^.*\///; $name; __ meta::function('node', <<'__'); # Runs node on a collection of source files and arguments. The format is: # node([@source_strings], @process_args); my ($sources, @args) = @_; with_exported(@$sources, sub { hook('before-node', $_[0], @args); sh('node', $_[0], @args); hook('after-node', $_[0], @args); }); __ meta::function('node-custom', <<'__'); # Runs node on a collection of source files and arguments. The format is: # &{'node-custom'}([@source_strings], [@node_arguments], @process_args); my ($sources, $node_args, @args) = @_; with_exported(@$sources, sub { hook('before-node-custom', @$node_args, $_[0], @args); sh('node', @$node_args, $_[0], @args); hook('after-node-custom', @$node_args, $_[0], @args); }); __ meta::function('parents', 'join "\\n", grep s/^parent:://o, sort keys %data;'); meta::function('perl', <<'__'); my $result = eval(join ' ', @_); $@ ? terminal::error($@) : $result; __ meta::function('reload', 'around_hook(\'reload\', sub {execute($_) for grep ! /^bootstrap::/, keys %data});'); meta::function('render', <<'__'); hook('before-render'); hook('after-render'); __ meta::function('rm', <<'__'); around_hook('rm', @_, sub { exists $data{$_} or terminal::warning("$_ does not exist") for @_; delete @data{@_}}); __ meta::function('run-forever', <<'__'); # Runs your application indefinitely, restarting each time it fails. # There's a one-second delay between restarts to prevent a tight loop. # Takes one argument, which is the function to run forever. my ($f, @args) = @_; hook('bin/before-run-forever'); &$f(@args) while sleep 0.1 && ! -f 'stop'; hook('bin/after-run-forever'); __ meta::function('save', 'around_hook(\'save\', sub {dangerous(\'\', sub {file::write($0, serialize()); $transient{initial} = state()}) if verify()});'); meta::function('save-state', <<'__'); # Creates a named copy of the current state and stores it. my ($state_name) = @_; around_hook('save-state', $state_name, sub { associate("state::$state_name", &{'current-state'}(), execute => 1)}); __ meta::function('sdoc', <<'__'); # Applies SDoc processing to a file or attribute. Takes the file or attribute # name as the first argument and returns the processed text. my %comments_for_extension = qw|c /*,*/ cpp // cc // h // java // py # rb # pl # pm # ml (*,*) js // hs -- sh # lisp ;;; lsp ;;; s ; scm ;;; sc ;;; as // html mli (*,*) cs // vim " elisp ; bas ' ada -- asm ; awk # bc # boo # tex % fss (*,*) erl % scala // hx // io // j NB. lua -- n // m % php // sql -- pov // pro % r # self "," tcl # texi @c tk # csh # vala // vbs ' v /*,*/ vhdl -- ss ;;; haml -# sass /*,*/ scss /*,*/ css /*,*/ fig /|; # No extension suggests a shebang line, which generally requires # to denote a comment. $comments_for_extension{''} = '#'; my $generated_string = 'Generated by SDoc'; sub is_code {map /^\s*[^A-Z\|\s]/o, @_} sub is_blank {map /^\n/o, @_} sub comment {my ($text, $s, $e) = @_; join "\n", map("$s $_$e", split /\n/, $text)} sub paragraphs {map split(/(\n{2,})/, $_), @_} my ($filename) = @_; # Two possibilities here. One is that the filename is an attribute, in which case # we want to look up the extension in the transients table. The other is that # it's a real filename. my ($extension) = $filename =~ /\.sdoc$/io ? $filename =~ /\.(\w+)\.sdoc$/igo : $filename =~ /\.(\w+)$/igo; my ($other_extension) = extension_for(attribute($filename)); $other_extension =~ s/^\.//o; my ($start, $end) = split /,/o, $comments_for_extension{lc($other_extension || $extension)}; join '', map(is_code($_) || is_blank($_) ? ($_ =~ /^\s*c\n(.*)$/so ? $1 : $_) : comment($_, $start, $end), paragraphs retrieve($filename)), "\n$start $generated_string $end\n"; __ meta::function('sdoc-html', <<'__'); # Converts SDoc to logically-structured HTML. Sections end up being nested, # and code sections and examples are marked as such. For instance, here is some # sample output: #
#

Foo

#

This is a paragraph...

#

This is another paragraph...

#
int main () {return 0;}
#
int main () {return 0} // Won't compile
#
#

Bar

# ... #
#
# It is generally good about escaping things that would interfere with HTML, # but within text paragraphs it lets you write literal HTML. The heuristic is # that known tags that are reasonably well-formed are allowed, but unknown ones # are escaped. my ($attribute) = @_; my @paragraphs = split /\n(?:\s*\n)+/, retrieve($attribute); my $known_tags = join '|', qw[html head body meta script style link title div a span input button textarea option select form label iframe blockquote code caption table tbody tr td th thead tfoot img h1 h2 h3 h4 h5 h6 li ol ul noscript p pre samp sub sup var canvas audio video]; my $section_level = 0; my @markup; my $indent = sub {' ' x ($_[0] || $section_level)}; my $unindent = sub {my $spaces = ' ' x ($section_level - 1); s/^$spaces//gm}; my $escape_all = sub {s/&/&/g; s//>/g}; my $escape_some = sub {s/&/&/g; s/<(?!\/|($known_tags)[^>]*>.*<\/\1>)/</gs}; my $code = sub {&$escape_all(); &$unindent(); s/^c\n//; push @markup, &$indent() . "
$_
"}; my $quoted = sub {&$escape_all(); &$unindent(); s/^\|(\s?)/ \1/; s/^ //mg; push @markup, &$indent() . "
$_
"}; my $paragraph = sub {&$escape_some(); push @markup, &$indent() . "

$_

"}; my $section = sub {my $h = $_[0] > 6 ? 6 : $_[0]; push @markup, &$indent($_[0] - 1) . "
", &$indent($_[0]) . "$2"}; my $close_section = sub {push @markup, &$indent($_[0]) . "
"}; my $title = sub { my $indentation = (length($1) >> 1) + 1; &$close_section($section_level) while $section_level-- >= $indentation; &$section($indentation); $section_level = $indentation; }; for (@paragraphs) { &$code(), next unless /^\h*[A-Z|]/; &$quoted(), next if /^\h*\|/; &$title(), s/^.*\n// if /^(\s*)(\S.*)\.\n([^\n]+)/ and length("$1$2") - 10 < length($3); &$paragraph(); } &$close_section($section_level) while $section_level--; join "\n", @markup; __ meta::function('sdocp', <<'__'); # Renders an attribute as SDocP. This logic was taken directly from the sdoc script. my $attribute = retrieve($_[0]); sub escape {my @results = map {s/\\/\\\\/go; s/\n/\\n/go; s/'/\\'/go; $_} @_; wantarray ? @results : $results[0]} "sdocp('" . escape($_[0]) . "', '" . escape($attribute) . "');"; __ meta::function('serialize', <<'__'); my ($options, @criteria) = separate_options(@_); my $partial = $$options{'-p'}; my $criteria = join '|', @criteria; my @attributes = map serialize_single($_), select_keys(%$options, '-m' => 1, '--criteria' => $criteria), select_keys(%$options, '-M' => 1, '--criteria' => $criteria); my @final_array = @{$partial ? \@attributes : [retrieve('bootstrap::initialization'), @attributes, 'internal::main();', '', '__END__']}; join "\n", @final_array; __ meta::function('serialize_single', <<'__'); # Serializes a single attribute and optimizes for content. my $name = $_[0] || $_; my $contents = $data{$name}; my $meta_function = 'meta::' . namespace($name); my $invocation = attribute($name); my $escaped = $contents; $escaped =~ s/\\/\\\\/go; $escaped =~ s/'/\\'/go; return "$meta_function('$invocation', '$escaped');" unless $escaped =~ /\v/; my $delimiter = '__' . fast_hash($contents); my $chars = 2; ++$chars until $chars >= length($delimiter) || index("\n$contents", "\n" . substr($delimiter, 0, $chars)) == -1; $delimiter = substr($delimiter, 0, $chars); "$meta_function('$invocation', <<'$delimiter');\n$contents\n$delimiter"; __ meta::function('sh', 'system(@_);'); meta::function('shell', <<'__'); terminal::cc(retrieve('data::current-continuation')) if length $data{'data::current-continuation'}; shell::repl(); __ meta::function('size', <<'__'); my $size = 0; $size += length $data{$_} for keys %data; sprintf "% 7d % 7d", length(serialize()), $size; __ meta::function('snapshot', <<'__'); my ($name) = @_; file::write(my $finalname = temporary_name($name), serialize(), noclobber => 1); chmod 0700, $finalname; hook('snapshot', $finalname); __ meta::function('state', <<'__'); my @keys = sort keys %data; my $hash = fast_hash(fast_hash(scalar @keys) . join '|', @keys); $hash = fast_hash("$data{$_}|$hash") for @keys; $hash; __ meta::function('test', <<'__'); # Runs a single test case. render(); node(['cached_dependency::caterwaul.all.js', 'js::montenegro.server', "test/$_[0].js"]); __ meta::function('touch', 'associate($_, \'\') for @_;'); meta::function('unlock', 'hook(\'unlock\', chmod_self(sub {$_[0] | 0200}));'); meta::function('update', '&{\'update-from\'}(@_, grep s/^parent:://o, sort keys %data);'); meta::function('update-from', <<'__'); # Upgrade all attributes that aren't customized. Customization is defined when the data type is created, # and we determine it here by checking for $transient{inherit}{$type}. # Note that this assumes you trust the remote script. If you don't, then you shouldn't update from it. around_hook('update-from-invocation', separate_options(@_), sub { my ($options, @targets) = @_; my %parent_id_cache = cache('parent-identification'); my %already_seen; @targets or return; my @known_targets = grep s/^parent:://, parent_ordering(map "parent::$_", grep exists $data{"parent::$_"}, @targets); my @unknown_targets = grep ! exists $data{"parent::$_"}, @targets; @targets = (@known_targets, @unknown_targets); my $save_state = ! ($$options{'-n'} || $$options{'--no-save'}); my $no_parents = $$options{'-P'} || $$options{'--no-parent'} || $$options{'--no-parents'}; my $force = $$options{'-f'} || $$options{'--force'}; my $clobber_divergent = $$options{'-D'} || $$options{'--clobber-divergent'}; &{'save-state'}('before-update') if $save_state; for my $target (@targets) { dangerous("updating from $target", sub { around_hook('update-from', $target, sub { my $identity = $parent_id_cache{$target} ||= join '', qx($target identity); next if $already_seen{$identity}; $already_seen{$identity} = 1; my $attributes = join '', qx($target ls -ahiu); my %divergent; die "skipping unreachable $target" unless $attributes; for my $to_rm (split /\n/, retrieve("parent::$target")) { my ($name, $hash) = split(/\s+/, $to_rm); next unless exists $data{$name}; my $local_hash = fast_hash(retrieve($name)); if ($clobber_divergent or $hash eq $local_hash or ! defined $hash) {rm($name)} else {terminal::info("preserving local version of divergent attribute $name (use update -D to clobber it)"); $divergent{$name} = retrieve($name)}} associate("parent::$target", $attributes) unless $no_parents; dangerous('', sub {eval qx($target serialize -ipmu)}); dangerous('', sub {eval qx($target serialize -ipMu)}); map associate($_, $divergent{$_}), keys %divergent unless $clobber_divergent; reload()})})} cache('parent-identification', %parent_id_cache); if (verify()) {hook('update-from-succeeded', $options, @targets); terminal::info("Successfully updated. Run 'load-state before-update' to undo this change.") if $save_state} elsif ($force) {hook('update-from-failed', $options, @targets); terminal::warning('Failed to verify: at this point your object will not save properly, though backup copies will be created.', 'Run "load-state before-update" to undo the update and return to a working state.') if $save_state} else {hook('update-from-failed', $options, @targets); terminal::error('Verification failed after the upgrade was complete.'); terminal::info("$0 has been reverted to its pre-upgrade state.", "If you want to upgrade and keep the failure state, then run 'update-from $target --force'.") if $save_state; return &{'load-state'}('before-update') if $save_state}}); __ meta::function('usage', '"Usage: $0 action [arguments]\\nUnique actions (run \'$0 ls\' to see all actions):" . ls(\'-u\');'); meta::function('verify', <<'__'); file::write(my $other = $transient{temporary_filename} = temporary_name(), my $serialized_data = serialize()); chomp(my $observed = join '', qx|perl '$other' state|); unlink $other if my $result = $observed eq (my $state = state()); terminal::error("Verification failed; expected $state but got $observed from $other") unless $result; hook('after-verify', $result, observed => $observed, expected => $state); $result; __ meta::hook('after-render::cleanup', 'unlink qw/minify.js minify.js.sdocp/;'); meta::hook('after-render::minify', 'minify($_) for qw/montenegro.client.js montenegro.server.js/;'); meta::internal_function('around_hook', <<'__'); # around_hook('hookname', @args, sub { # stuff; # }); # Invokes 'before-hookname' on @args before the sub runs, invokes the # sub on @args, then invokes 'after-hookname' on @args afterwards. # The after-hook is not invoked if the sub calls 'die' or otherwise # unwinds the stack. my $hook = shift @_; my $f = pop @_; hook("before-$hook", @_); my $result = &$f(@_); hook("after-$hook", @_); $result; __ meta::internal_function('associate', <<'__'); my ($name, $value, %options) = @_; die "Namespace does not exist" unless exists $datatypes{namespace($name)}; $data{$name} = $value; execute($name) if $options{'execute'}; $value; __ meta::internal_function('attribute', <<'__'); my ($name) = @_; $name =~ s/^[^:]*:://; $name; __ meta::internal_function('attribute_is', <<'__'); my ($a, %options) = @_; my %inherited = parent_attributes(grep /^parent::/o, sort keys %data) if grep exists $options{$_}, qw/-u -U -d -D/; my $criteria = $options{'--criteria'} || $options{'--namespace'} && "^$options{'--namespace'}::" || '.'; my %tests = ('-u' => sub {! $inherited{$a}}, '-d' => sub {$inherited{$a} && fast_hash(retrieve($a)) ne $inherited{$a}}, '-i' => sub {$transient{inherit}{namespace($a)}}, '-s' => sub {$a =~ /^state::/o}, '-m' => sub {$a =~ /^meta::/o}); return 0 unless scalar keys %tests == scalar grep ! exists $options{$_} || &{$tests{$_}}(), keys %tests; return 0 unless scalar keys %tests == scalar grep ! exists $options{uc $_} || ! &{$tests{$_}}(), keys %tests; $a =~ /$criteria/ __ meta::internal_function('cache', <<'__'); my ($name, %pairs) = @_; if (%pairs) {associate("cache::$name", join "\n", map {$pairs{$_} =~ s/\n//g; "$_ $pairs{$_}"} sort keys %pairs)} else {map split(/\s/, $_, 2), split /\n/, retrieve("cache::$name")} __ meta::internal_function('chmod_self', <<'__'); my ($mode_function) = @_; my (undef, undef, $mode) = stat $0; chmod &$mode_function($mode), $0; __ meta::internal_function('dangerous', <<'__'); # Wraps a computation that may produce an error. my ($message, $computation) = @_; terminal::info($message) if $message; my @result = eval {&$computation()}; terminal::warning(translate_backtrace($@)), return undef if $@; wantarray ? @result : $result[0]; __ meta::internal_function('debug_trace', <<'__'); terminal::debug(join ', ', @_); wantarray ? @_ : $_[0]; __ meta::internal_function('dep', <<'__'); # A variadic function to prepend cached_dependency:: onto things. # Used like this: dep(qw/caterwaul.all.js montenegro.server.js/) map "cached_dependency::$_", @_; __ meta::internal_function('execute', <<'__'); my ($name, %options) = @_; my $namespace = namespace($name); eval {&{"meta::$namespace"}(attribute($name), retrieve($name))}; warn $@ if $@ && $options{'carp'}; __ meta::internal_function('exported', <<'__'); # Allocates a temporary file containing the concatenation of attributes you specify, # and returns the filename. The filename will be safe for deletion anytime. my $filename = temporary_name(); file::write($filename, cat(@_)); $filename; __ meta::internal_function('extension_for', <<'__'); my $extension = $transient{extension}{namespace($_[0])}; $extension = &$extension($_[0]) if ref $extension eq 'CODE'; $extension || ''; __ meta::internal_function('fast_hash', <<'__'); my ($data) = @_; my $piece_size = length($data) >> 3; my @pieces = (substr($data, $piece_size * 8) . length($data), map(substr($data, $piece_size * $_, $piece_size), 0 .. 7)); my @hashes = (fnv_hash($pieces[0])); push @hashes, fnv_hash($pieces[$_ + 1] . $hashes[$_]) for 0 .. 7; $hashes[$_] ^= $hashes[$_ + 4] >> 16 | ($hashes[$_ + 4] & 0xffff) << 16 for 0 .. 3; $hashes[0] ^= $hashes[8]; sprintf '%08x' x 4, @hashes[0 .. 3]; __ meta::internal_function('file::read', <<'__'); my $name = shift; open my($handle), "<", $name; my $result = join "", <$handle>; close $handle; $result; __ meta::internal_function('file::write', <<'__'); use File::Path 'mkpath'; use File::Basename 'dirname'; my ($name, $contents, %options) = @_; die "Choosing not to overwrite file $name" if $options{noclobber} and -f $name; mkpath(dirname($name)) if $options{mkpath}; open my($handle), $options{append} ? '>>' : '>', $name or die "Can't open $name for writing"; print $handle $contents; close $handle; __ meta::internal_function('fnv_hash', <<'__'); # A rough approximation to the Fowler-No Voll hash. It's been 32-bit vectorized # for efficiency, which may compromise its effectiveness for short strings. my ($data) = @_; my ($fnv_prime, $fnv_offset) = (16777619, 2166136261); my $hash = $fnv_offset; my $modulus = 2 ** 32; $hash = ($hash ^ ($_ & 0xffff) ^ ($_ >> 16)) * $fnv_prime % $modulus for unpack 'L*', $data . substr($data, -4) x 8; $hash; __ meta::internal_function('hypothetically', <<'__'); # Applies a temporary state and returns a serialized representation. # The original state is restored after this, regardless of whether the # temporary state was successful. my %data_backup = %data; my ($side_effect) = @_; my $return_value = eval {&$side_effect()}; %data = %data_backup; die $@ if $@; $return_value; __ meta::internal_function('internal::main', <<'__'); disable(); $SIG{'INT'} = sub {snapshot(); exit 1}; $transient{initial} = state(); chomp(my $default_action = retrieve('data::default-action')); my $function_name = shift(@ARGV) || $default_action || 'usage'; terminal::warning("unknown action: '$function_name'") and $function_name = 'usage' unless $externalized_functions{$function_name}; around_hook('main-function', $function_name, @ARGV, sub { dangerous('', sub { chomp(my $result = &$function_name(@ARGV)); print "$result\n" if $result})}); save() unless state() eq $transient{initial}; END { enable(); } __ meta::internal_function('invoke_editor_on', <<'__'); my ($data, %options) = @_; my $editor = $options{editor} || $ENV{VISUAL} || $ENV{EDITOR} || die 'Either the $VISUAL or $EDITOR environment variable should be set to a valid editor'; my $options = $options{options} || $ENV{VISUAL_OPTS} || $ENV{EDITOR_OPTS} || ''; my $attribute = $options{attribute}; $attribute =~ s/\//-/g; my $filename = temporary_name() . "-$attribute$options{extension}"; file::write($filename, $data); system("$editor $options '$filename'"); my $result = file::read($filename); unlink $filename; $result; __ meta::internal_function('is_locked', '!((stat($0))[2] & 0222);'); meta::internal_function('namespace', <<'__'); my ($name) = @_; $name =~ s/::.*$//; $name; __ meta::internal_function('parent_attributes', <<'__'); my $attributes = sub {my ($name, $value) = split /\s+/o, $_; $name => ($value || 1)}; map &$attributes(), split /\n/o, join("\n", retrieve(@_)); __ meta::internal_function('parent_ordering', <<'__'); # Topsorts the parents by dependency chain. The simplest way to do this is to # transitively compute the number of parents referred to by each parent. my @parents = @_; my %all_parents = map {$_ => 1} @parents; my %parents_of = map { my $t = $_; my %attributes = parent_attributes($_); $t => [grep /^parent::/, keys %attributes]} @parents; my %parent_count; my $parent_count; $parent_count = sub { my ($key) = @_; return $parent_count{$key} if exists $parent_count{$key}; my $count = 0; $count += $parent_count->($_) + exists $data{$_} for @{$parents_of{$key}}; $parent_count{$key} = $count}; my %inverses; push @{$inverses{$parent_count->($_)} ||= []}, $_ for @parents; grep exists $all_parents{$_}, map @{$inverses{$_}}, sort keys %inverses; __ meta::internal_function('retrieve', <<'__'); my @results = map defined $data{$_} ? $data{$_} : retrieve_with_hooks($_), @_; wantarray ? @results : $results[0]; __ meta::internal_function('retrieve_with_hooks', <<'__'); # Uses the hooks defined in $transient{retrievers}, and returns undef if none work. my ($attribute) = @_; my $result = undef; defined($result = &$_($attribute)) and return $result for map $transient{retrievers}{$_}, sort keys %{$transient{retrievers}}; return undef; __ meta::internal_function('select_keys', <<'__'); my %options = @_; grep attribute_is($_, %options), sort keys %data; __ meta::internal_function('separate_options', <<'__'); # Things with one dash are short-form options, two dashes are long-form. # Characters after short-form are combined; so -auv4 becomes -a -u -v -4. # Also finds equivalences; so --foo=bar separates into $$options{'--foo'} eq 'bar'. # Stops processing at the -- option, and removes it. Everything after that # is considered to be an 'other' argument. # The only form not supported by this function is the short-form with argument. # To pass keyed arguments, you need to use long-form options. my @parseable; push @parseable, shift @_ until ! @_ or $_[0] eq '--'; my @singles = grep /^-[^-]/, @parseable; my @longs = grep /^--/, @parseable; my @others = grep ! /^-/, @parseable; my @singles = map /-(.{2,})/ ? map("-$_", split(//, $1)) : $_, @singles; my %options; $options{$1} = $2 for grep /^([^=]+)=(.*)$/, @longs; ++$options{$_} for grep ! /=/, @singles, @longs; ({%options}, @others, @_); __ meta::internal_function('strip', 'wantarray ? map {s/^\\s*|\\s*$//g; $_} @_ : $_[0] =~ /^\\s*(.*?)\\s*$/ && $1;'); meta::internal_function('table_display', <<'__'); # Displays an array of arrays as a table; that is, with alignment. Arrays are # expected to be in column-major order. sub maximum_length_in { my $maximum = 0; length > $maximum and $maximum = length for @_; $maximum; } my @arrays = @_; my @lengths = map maximum_length_in(@$_), @arrays; my @row_major = map {my $i = $_; [map $$_[$i], @arrays]} 0 .. $#{$arrays[0]}; my $format = join ' ', map "%-${_}s", @lengths; join "\n", map strip(sprintf($format, @$_)), @row_major; __ meta::internal_function('temporary_name', <<'__'); use File::Temp 'tempfile'; my (undef, $temporary_filename) = tempfile("$0." . 'X' x 4, OPEN => 0); $temporary_filename; __ meta::internal_function('translate_backtrace', <<'__'); my ($trace) = @_; $trace =~ s/\(eval (\d+)\)/$locations{$1 - 1}/g; $trace; __ meta::internal_function('with_exported', <<'__'); # Like exported(), but removes the file after running some function. # Usage is with_exported(@files, sub {...}); my $f = pop @_; my $name = exported(@_); my $result = eval {&$f($name)}; terminal::warning("$@ when running with_exported()") if $@; unlink $name; $result; __ meta::library('shell', <<'__'); # Functions for shell parsing and execution. package shell; use Term::ReadLine; sub tokenize {grep length, split /\s+|("[^"\\]*(?:\\.)?")/o, join ' ', @_}; sub parse { my ($fn, @args) = @_; s/^"(.*)"$/\1/o, s/\\\\"/"/go for @args; {function => $fn, args => [@args]}} sub execute { my %command = %{$_[0]}; die "undefined command: $command{function}" unless exists $externalized_functions{$command{function}}; &{"::$command{function}"}(@{$command{args}})} sub run {execute(parse(tokenize(@_)))} sub prompt { my %options = @_; my $name = $options{name} // ::name(); my $state = $options{state} // ::state(); my $other = $state ne $transient{initial} ? 33 : 30; my $locked = ::is_locked() ? "\033[1;31mlocked\033[0;0m" : ''; my $cc = length ::retrieve('data::current-continuation') ? "\033[1;36mcc\033[0;0m" : ''; "\033[1;32m$name\033[1;${other}m" . substr($state, 0, 4) . "\033[0;0m$cc$locked\033[1;34m$options{stuff}\033[0;0m "} sub repl { my %options = @_; my $term = new Term::ReadLine "$0 shell"; $term->ornaments(0); my $attribs = $term->Attribs; $attribs->{completion_entry_function} = $attribs->{list_completion_function}; my $autocomplete = $options{autocomplete} || sub {[sort keys %data, sort keys %externalized_functions]}; my $prompt = $options{prompt} || \&prompt; my $parse = $options{parse} || sub {parse(tokenize(@_))}; my $command = $options{command} || sub {my ($command) = @_; ::around_hook('shell-command', $command, sub {print ::dangerous('', sub {execute($command)}), "\n"})}; &$command(&$parse($_)) while ($attribs->{completion_word} = &$autocomplete(), defined($_ = $term->readline(&$prompt())))} __ meta::library('terminal', <<'__'); # Functions for nice-looking terminal output. package terminal; my $process = ::name(); sub message {print STDERR "[$_[0]] $_[1]\n"} sub color { my ($name, $color) = @_; *{"terminal::$name"} = sub {chomp($_), print STDERR "\033[1;30m$process(\033[1;${color}m$name\033[1;30m)\033[0;0m $_\n" for map join('', $_), @_}} my %preloaded = (info => 32, progress => 32, state => 34, debug => 34, warning => 33, error => 31); color $_, $preloaded{$_} for keys %preloaded; __ meta::message_color('cc', '36'); meta::message_color('state', 'purple'); meta::message_color('states', 'yellow'); meta::message_color('test', 'purple'); meta::parent('/home/spencertipping/bin/configuration', <<'__'); meta::type::configuration d67e10a128e6b1d958c5b9d3bbe25aa4 parent::/home/spencertipping/bin/object 9088a9cf9c95b7c1184f1c3b528824ba __ meta::parent('/home/spencertipping/bin/node-base', <<'__'); function::loc 36e0cabf1fe1c1bcaa3c8c708bd82ca0 function::node 3522fde8f76947a5f70bca33f1ee0016 function::node-custom c2f4063798c997ec7f78a3543b1240b3 function::render 4962b4a2e6bc800fd5c11b97550b623e function::run-forever 76175932a2d2692fc802856c28d0848d internal_function::dep bad9b934374b176318ed2295b63130bc message_color::test 14e993fdf2c62df353613c243dc9053b meta::type::js f7a0080116fecebf7abb030b0156f44d parent::/home/spencertipping/bin/repository 0d317b0c917f59703ee01bf3cf03ad13 parent::/home/spencertipping/conjectures/perl-objects/sdoc e3fc5cfcb42fc929d07a588023f9be5d __ meta::parent('/home/spencertipping/bin/object', <<'__'); bootstrap::html 9b839f4c0c8d6bb18bf6d9d0e670e497 bootstrap::initialization b39f25c213ceb0418551084d1bddeb20 bootstrap::perldoc c63395cbc6f7160b603befbb2d9b6700 function::alias 28744564997657da45ab16cd5b441104 function::cat a19fdfda461f9a0aa01978dff2e2c2f7 function::cc c4d52b1d8f52a480f07b81b93c3aac7b function::ccc 2351344fc688518c75aa4ab3acec1c4a function::child f69646398c3123d3d939a7f2b3156606 function::clone 54e00ff2103e54423d4c9febb97ce063 function::cp e5fee448a74ecbf4ae215e6b43dfc048 function::create 090c342a2dc304b39c643d53350474a0 function::current-state 48c98d3bd74f3ee14d358fd127a7867a function::disable 49db26fcd680ca34c1f52629a7375d2f function::edit c31d73791820a10ec910af043f67d4eb function::enable 37af2d2e603e2455be227ae3c5a42c3a function::export 388e0cc60507443cb1c0cc3e2658cfef function::extern 6a9fa8c2a8a4eaae9fe8d38e402145e4 function::grep 3ad630ca30f1abae3cf28ef6293f0e00 function::hash 7a903f90f2c8ed27af7e030f407d9f7b function::hook d74d8e2b611342af6a0897e0bd62e6e6 function::hooks 230122bdc8929884e45b2f78a7743e2e function::identity 37106ce13a0200af001d361ce7e81e57 function::import ac86cbe9c9fb12fc8cef2cc88e80c01e function::initial-state e21ba3519838b221a7b2d4e8a7544e7f function::is 1d3401965e4720ed972470b54b447db0 function::load-state ea18db867bd62a294e067f60e6975dcf function::lock 9bf21fee2f0f809131d43553bde82fa5 function::ls 61a247f0a582f63d85b5d5a963a46893 function::mv 52e95180e3c7019116bd798e0da0fdda function::name 6848cbc257e4b6d7441b25acb04e23c9 function::parents f94c3a5addbc92fe7f90d198fa701484 function::perl 986a274c013b77fe08d29726ce3799fe function::reload c57ff432c3ffd91a5506cd3eb8bf50c9 function::rm 7cecfb1691a7bf86741f00058bcc54ca function::save 181da4858ac39c157dbf38e6bac7a0d2 function::save-state 863e4d9fa75ca198ef7a755248d1002a function::serialize 5148e8ca46eeb3e297f76d098e496bcf function::serialize_single e7a22f806b580d5c63982ffc7f218a1a function::sh 9647fa9227bef6c139a79d0dd4acc8b4 function::shell dc8238ad70b1e02eaf230c4f1bd21690 function::size 140728e163286a0f3f514f468c475cfc function::snapshot d3d84a364524eeb8ee90623f545187e8 function::state 119111f84c3e32a5536838ac84bc6f10 function::touch 819878bc64df094d3323c7050f2c3e97 function::unlock 8fc9bd69f3466f0b54ee2c6965f68cea function::update 4de1a6a4085836590a3b1ef997f9d5ea function::update-from beead29d50b0b204bc9a8dc6f07d0003 function::usage b36ead828ad566c8e3919f3b40fb99e6 function::verify d31b85fffd464ddf516d2afeb63dcbde internal_function::around_hook e1cd17b80d4e8165df9c94facd9f239b internal_function::associate fc4f785bcf3ffe3225a73a1fdd314703 internal_function::attribute 62efb9f22157835940af1d5feae98d98 internal_function::attribute_is c88e0f87cf807ab0af73ca10032b54b2 internal_function::cache c119f9d7ea9a147c6d526a6283fb119a internal_function::chmod_self b13487447c65f2dc790bd6b21dde89dd internal_function::dangerous 4b8343178d6d4d1b760d61b1cfda008c internal_function::debug_trace 77644ab45a770a6e172680f659911507 internal_function::execute 4b4efc33bc6767a7aade7f427eedf83f internal_function::exported 27414e8f2ceeaef3555b9726e690eb0f internal_function::extension_for 65e48f50f20bc04aa561720b03bf494c internal_function::fast_hash ac70f469e697725cfb87629833434ab1 internal_function::file::read 186bbcef8f6f0dd8b72ba0fdeb1de040 internal_function::file::write eb7b1efebe0db73378b0cce46681788d internal_function::fnv_hash 8d001a3a7988631bab21a41cee559758 internal_function::hypothetically 33ee2e1595d3877bd1d9accaa72305c8 internal_function::internal::main 435a9e83ac803960745d9aa5aac6c75f internal_function::invoke_editor_on 1448132d5294a4b8390b4a684d8a78f9 internal_function::is_locked 42c3c89625863a31105d1df49a2a762f internal_function::namespace 93213d60cafb9627e0736b48cd1f0760 internal_function::parent_attributes 480776f176ab728c6b7450287be5782a internal_function::parent_ordering 87bee77390de5b9e9c6e07f9af9eb70a internal_function::retrieve 0b6f4342009684fdfa259f45ac75ae37 internal_function::retrieve_with_hooks 5186a0343624789d08d1cc2084550f3d internal_function::select_keys 1cd075b241905d9ab3199e680e86dced internal_function::separate_options d47e8ee23fe55e27bb523c9fcb2f5ca1 internal_function::strip 4af6a470effeed94c0dd9800d01f7d66 internal_function::table_display 8a6897e093f36bf05477a3889b84a61d internal_function::temporary_name 0fb1402061581b69822f913631b4a9d9 internal_function::translate_backtrace 06fad3d85833a6484e426401b95e0206 internal_function::with_exported fc4f32c46d95c6deed0414364d1c7410 library::shell 528f486cc4d9eb390e4c350b8727c751 library::terminal c52308d05ebb4ff61c5fc36e6d9c7a8a message_color::cc 6249446f73b3c5af2404c322c150e57b message_color::state 14e993fdf2c62df353613c243dc9053b message_color::states 152a940086f7cee6110528a09af7dd78 meta::configure 25976e07665878d3fae18f050160343f meta::externalize 9141b4e8752515391385516ae94b23b5 meta::functor::editable e3d2ede6edf65ffe2123584b2bd5dab7 meta::type::alias 28fe15dd61f4902ed5180d8604d15d97 meta::type::bootstrap 297d03fb32df03b46ea418469fc4e49e meta::type::cache 52eac0d7b550a358cc86803fe1a9d921 meta::type::data 58d8027f20099b28a159eaac67314051 meta::type::function d93b3cc15693707dac518e3d6b1f5648 meta::type::hook f55a3f728ddfb90204dff3fe5d86845c meta::type::inc c95915391b969734305f2f492d5ca8e3 meta::type::internal_function 34abb44c67c7e282569e28ef6f4d62ab meta::type::library b6dd78120e6d787acdb5c1629f7f1896 meta::type::message_color 794bf137c425293738f07636bcfb5c55 meta::type::meta 640f25635ce2365b0648962918cf9932 meta::type::parent 607e9931309b1b595424bedcee5dfa45 meta::type::retriever 6e847a9d205e4a5589765a3366cdd115 meta::type::state c1f29670be26f1df6100ffe4334e1202 retriever::file b8e7aefc98b8341260d91f21dc61d749 retriever::id a791a5735e9b4f2cb8b99fd39dc17bc3 __ meta::parent('/home/spencertipping/bin/repository', <<'__'); function::dupdate 1c0273217c5b9f2756bb14a4a00aa7e2 meta::type::cached_dependency e9455b403cbff27bbcc41d917fef482f parent::/home/spencertipping/bin/configuration e4d22dc2215115982f6b79ed0ce22a42 __ meta::parent('/home/spencertipping/conjectures/perl-objects/sdoc', <<'__'); function::sdoc 060cfa349e629eb90a82b87a8ba00c1d function::sdoc-html e53ef666b3ae25af0050c3ed582fca8e function::sdocp 8b7ed5bbd537234ae53c0691b6d02c97 meta::type::sdoc 392c65eddae300e2aa67014b85884979 parent::/home/spencertipping/bin/object 9088a9cf9c95b7c1184f1c3b528824ba retriever::html-sdoc 54d22ab83d9f3f2e27ef51033b4817a7 retriever::sdoc 6de8ec1f1436fb8e7477b533c321081b retriever::sdocp fef74cd94fa8761618662802f0bfc171 __ meta::retriever('file', '-f $_[0] ? file::read($_[0]) : undef;'); meta::retriever('html-sdoc', <<'__'); my ($attribute) = @_; return undef unless $attribute =~ s/^html::/sdoc::/ and exists $data{$attribute}; &{'sdoc-html'}($attribute); __ meta::retriever('id', '$_[0] =~ /^id::/ ? substr($_[0], 4) : undef;'); meta::retriever('sdoc', 'exists $data{"sdoc::$_[0]"} ? sdoc("sdoc::$_[0]") : undef;'); meta::retriever('sdocp', <<'__'); my $attribute = attribute($_[0]); exists $data{"sdoc::$attribute"} ? sdocp("sdoc::$attribute") : undef; __ meta::sdoc('js::minify', <<'__'); Minifies a JavaScript file by using the Caterwaul parse/deparse mechanism. This won't do identifier packing (which is unsafe in Caterwaul), but it will provide decent minification by removing comments and most whitespace. var code = require('fs').readFileSync(process.argv[2], 'utf8'); require('fs').writeFileSync(process.argv[2].replace(/\.js$/, '.min.js'), caterwaul.parse(code).serialize(), 'utf8'); __ meta::sdoc('js::montenegro.client', <<'__'); Montenegro client-side bindings | Spencer Tipping Licensed under the terms of the MIT source code license Introduction. Montenegro works with jQuery to add structure to user interfaces and define useful shorthands for common cases. It also provides a Javascript markup language that you can use to build interfaces instead of going the HTML route. For logic-heavy applications this may make more sense than separating the layout. JQuery extension methods. These are used throughout Montenegro. up() takes either a selector or a number. If you give it a selector, it grabs the nearest matching parent; if you give it a number, it traverses up that many parents. The nearest() method lets you do approximate matching. For example, suppose you have this setup: | div.foo(div > textarea, div > button.save) If you want the save button to see the textarea (e.g. from inside a click handler), the obvious solution is $(this).parent().find('textarea'). However, that's a lot of work and doesn't scale well if the DOM layout changes. Better is to say $(this).nearest('textarea'). Note that it doesn't actually return just the single nearest one. It just goes up until it starts finding textareas. Note that .nearest() is O(n^2) and average-case n log n in the number of nodes in the document. The cval() method returns and then clears the value of a component. If you give it a parameter, the value will be cleared to that value rather than set to the empty string. caterwaul.tconfiguration('std', 'montenegro.methods', function () {jQuery.fn /se[_.up(s) = s instanceof Number ? s ? this.parent().up(s - 1) : this : this.parents(s).eq(0), _.cval(nv) = l[v = this.val()] in this.val(nv || '') /re[v], _.nearest(s) = this.length ? this.find(s) /re[_.length ? _ : this.parent().nearest(s)] : jQuery([])]}). Event extensions. Some events are common enough that it's useful to have a handler for them. Hitting the enter key is one of those. Another is getting a link to have a click action and look active, but not actually go anywhere. tconfiguration('std', 'montenegro.events', function () {jQuery.fn /se[_.enter_key(f) = this.keyup(fn[e][f.call(this, e), when[e.which === 13]]), _.escape_key(f) = this.keyup(fn[e][f.call(this, e), when[e.which === 27]]), _.clickable(f) = this.attr('href', 'javascript:void(0)').click(f)]}). Fixes. These are fixes for places where jQuery is somehow suboptimal. Examples include extensions to support variadic/pluralized append(), prepend(), before(), and after(), and various fixes for the clone() method. These fixes are still maintained in other Github repositories: | http://github.com/spencertipping/jquery.fix.append-multiple http://github.com/spencertipping/jquery.fix.select-clone http://github.com/spencertipping/jquery.fix.textarea-clone tconfiguration('std seq continuation', 'montenegro.fixes', function () { $.from_many() = l[as = arguments] in $([]) /se.r[seq[~as *![_ instanceof Array || _ instanceof jQuery ? seq[~_ *![r.push(_)]] : r.push(_)]]], $.fn.clone() = original_clone.call(this) /se[clone_values_of_components(this, _)], $.fn.after = make_variadic_and_plural($.fn.after), $.fn.before = make_variadic_and_plural($.fn.before), $.fn.append = make_variadic_and_plural($.fn.append), $.fn.prepend = make_variadic_and_plural($.fn.prepend), where*[$ = jQuery, original_clone = $.clone, make_variadic_and_plural(f)() = l[xs = arguments] in this /se.t[seq[~xs *![_ instanceof Array ? make_variadic_and_plural(f).apply(t, _.slice()) : f.call(t, _), unless[_ == null]]]], clone_values_of_components(source, destination) = l*[needs_filling = 'select, textarea', paired = seq[~source.find(needs_filling) *+$ ^ destination.find(needs_filling) *+$]] in seq[paired *![_[1].val(_[0].val())]]]}). RPC tunneling. You can connect to a server endpoint with a CPS-converted proxy function. You can also send opaque references to the server (presumably so that it can send them back). Here's an example of passing a DOM node: | var identity = montenegro.rpc('/identity-function'); var body = $('body'); identity(montenegro.rpc.ref(body), fn[result][montenegro.rpc.ref(result).append('Got the body element back')]); // alternatively: l/cps[result <- identity(montenegro.rpc.ref(dom_node), _)][montenegro.rpc.ref(result).append('Got the body element back')]; Assuming that the server replies with the data it was given, this will append some text to the document body when the server replies. The mechanism for this is actually really simple; montenegro.rpc.ref() just assigns a new gensym to each value you alias; that string goes to the server and is later resolved back into the client-side value. (This is why the server won't be able to do anything useful with the value.) Montenegro automatically garbage-collects the reference table by deallocating a reference when you dereference it. (So you can't dereference something more than once; if you do this it will create a new reference instead.) Example: Building a chat client. In montenegro.server.js.sdoc there's an example of a broadcast chat server. Here's the corresponding client code and some DOM nodes to make it work: | var send = caterwaul.montenegro.rpc('/chat/send'); caterwaul.montenegro.rpc('/chat')(fn[message][$('.log').append(html[div.message(message)]), this()]); $('body').append(html[div(div.log, button('Send'), input])); // This just builds the UI. You could also do this with regular HTML. l/cps[_ <- $('button').click(_)][send($('#input').cval())]; The 'this()' invocation inside the callback is used when you want to send something back and reuse the callback function. I'm using it here to avoid having to refer to the callback function in a first-class way (which would normally be necessary to set the cycle up again). tconfiguration('std seq', 'montenegro.rpc', function () { this.namespace('montenegro') /se[ _.rpc(url)() = l[as = seq[~arguments]][l*[callback = as.length && as[as.length - 1] /re[_.constructor === Function && as.pop()]] in $.ajax({url: url, type: 'POST', contentType: 'application/json', data: JSON.stringify(as.slice()), dataType: 'json', success: fn[reply][callback && callback.apply(fn_[_.rpc(url).apply(null, seq[~arguments].slice().concat([callback]))], reply)]})]]}). DOM construction. You can build elements using a CSS-selector-style syntax. (Alternatively, you can provide an element in the markup; if you do it this way, the template element's ID should match the model name.) So, for example, suppose we're modeling a person with a name and e-mail address. Here's what the markup might look like in HTML: |
| html[div.person(input.name.nonempty, input.email, a.facebook /attr('href', 'http://facebook.com/someone') > 'A Facebook Page')] Note that you can't use hyphens in the class names in Javascript, but if you type underscores they'll be converted into dashes. For example: | html[div.first_name] // becomes
Automation and event handlers. You can get the jQuery shell for an element by using the '/' operator. The right-hand side is an invocation on the jQuery shell; for example: | $('').addClass('foo').click(fn_[...]).mouseover(fn_[...]) // can be written as: html[a.foo /click(fn_[...]).mouseover(fn_[...])] Anything after a '/' for an element is not considered to be HTML, so you'll have to use another html[] if you want to create elements to pass into a jQuery function. For example: | html[a.foo /append(span('some text'))] // won't do what you want html[a.foo /append(html[span('some text')])] // this is the right way to do it html[a.foo > span('some text')] // even better Evaluating subexpressions. Going back to the person example, suppose you have a list of people that you want to insert into a div. Here's what that looks like: | var people = seq[...]; var person = fn[p][html[div.person(input.name.nonempty /val(p.name))]]; var ui = html[div.people(people.map(person), button.save('Save'), button.cancel('Cancel'))]; Here, the expression 'people.map(person)' gets evaluated as a Javascript expression rather than as markup. Montenegro knows to do this because 'people' isn't one of the HTML elements it knows about. Javascript expressions should return strings, sequences, arrays, or jQuery objects. Strings get promoted into text nodes, so you don't have to worry about HTML escaping. Mapping. You can map an element through a function using the '%' shorthand. For example: | var nonempty = fn_[this.instavalidate(/^.+$/)]; var ui = html[div(input.name %nonempty, input.title %nonempty)] This isn't quite the same thing as side-effecting. Using the map shorthand replaces the element with whatever your map function returns, which may or may not be desirable. Note that tempting as it is, you can't say this: | html[div((input.name, input.title) %nonempty)] // can't do this, even though it would be awesome I considered adding a distributive property, but Javascript's syntax is restrictive enough that I don't think it makes sense. It also makes you think too hard about your markup, which isn't a good thing. The markup should be simple and local, and your modifier functions should be short enough to type several times. (This can be achieved by using a let-binding or similar.) Evaluation contexts. Containment can be specified either as div(x) or by div > x. If you want 'x' to be evaluated as Javascript code rather than HTML, you can use >=, for instance div >= x. (Think of <% vs. <%= in ERB or ASP.) Specifying multiple children is possible too; you use div >= [x, y, z]. (Using div >= (x, y, z) will evaluate (x, y, z) as JS, which returns just z.) Note that because > and >= are left-associative, a > b > c will add b and c to a rather than adding c to b, then b to a. Attributes. You can define attributes by using one form of the * syntax: | html[a.foo *href('http://www.google.com')] As usual, underscores are replaced by hyphens -- most HTML attributes don't contain underscores. Context inference. There's a little bit of guessing that goes on about what's what. Usually the guesser gets things right, but there are some older HTML nodes that it doesn't detect. For example: | html[div.foo > people.map(person)] // div.foo is an element, people.map(person) is a function call -- its return value will be appended to the div html[a.code > b.code > 'foo'] // a.code is a link with class 'code' that contains the Javascript value 'b.code' and the text 'foo' html[div, foo, bar(bif), code('bar')] // a div, the value 'foo', the function 'bar' called on bif, and a element containing the text 'bar' The complete list is in caterwaul.montenegro.dom.elements; setting additional keys in this hash to truthy values causes those identifiers to be treated as valid HTML elements. (I mention this because at the moment the HTML5 standard isn't completely listed.) Caveats. Sometimes context inference doesn't quite work right. One particular case is when you embed the seq[] macro inside html[] -- in this case, the html[] macro happily dives through the seq[] shell and into the expressions, interpreting things like seq[xs *[_ + 1]] as HTML invocations with attributes called []. (Obviously not the right thing to do.) To prevent this from happening, use forcing contexts such as >= and []. For example: | html[table(seq[~rows *[tr(td(_.name), td(_.value))]])] // This fails at compile-time html[table[seq[~rows *[html[tr(td(_.name, td(_.value))]]]]] // Forced context; this one works Subtleties of this macroexpander. There's only one thing that's particularly subtle and crucial to how this works. That's the detail of the qs[_] matcher, which is the first macro defined for the DOM expander. This macro isn't written as a conditional because we never want the macroexpansion to descend as it normally would. Rather, we drive the descent using explicit calls to macroexpand(). Therefore, qs[_] must always claim to have replaced the syntax with something; thus the failure case is just identity, indicating a success and no macro-driven descent. tconfiguration('std seq continuation', 'montenegro.dom', function () { this.configure('montenegro.fixes montenegro.methods').namespace('montenegro').dom = this.global().clone() /se[ this.rmacro(qs[html[_]], _) /cps.t[qs[jQuery.from_many(_x)].replace({_x: _.macroexpand(t)})], _.elements = this.util.qw('html head body meta script style link title div a span input button textarea option select form label iframe blockquote code caption ' + 'table tbody tr td th thead tfoot img h1 h2 h3 h4 h5 h6 li ol ul noscript p pre samp sub sup var canvas audio video') /re[seq[!(~_ *[[_, _]])]], l*[ref(x) = new this.ref(x), expand = _.macroexpand, is_an_element(tree) = _.elements[tree.data] || tree[0] && is_an_element(tree[0]), htmlify(s) = s.replace(/_/g, '-')] in _.macro /se[_(qs[_], fn[x][e ? qs[jQuery(document.createElement(_tag))].replace({_tag: '"#{e}"'}) : x, where[e = is_an_element(x)]]), _(qs[_(_)], appender(expand, expand)), _(qs[_[_]], appender(expand, id)), _(qs[_ > _], appender(expand, expand)), _(qs[_ >= _], appender(expand, id)), where[id(x) = x, appender(f, g)(t1, t2) = is_an_element(t1) && qs[_e.append(_c)].replace({_e: f(t1), _c: g(t2)})], _(qs[[_]], fn [t][qs[[_e]] .replace({_e: expand(t)})]), _(qs[_, _], fn[t1, t2][qs[_1, _2].replace({_1: expand(t1), _2: expand(t2)})]), _(qs[_ %_], fn[t1, t2][qs[_f(_e)].replace({_e: expand(t1), _f: t2})]), _(qs[_ /_], fn[t1, t2][qs[_e._f] .replace({_e: expand(t1), _f: t2})]), _(qs[_ *_(_)], fn[e, a, v][qs[_e.attr(_a, _v)].replace({_e: expand(e), _a: '"#{htmlify(a.data)}"', _v: v}), when[is_an_element(e)]]), _(qs[_._], fn [t1, t2][qs[_e.addClass(_c)].replace({_e: expand(t1), _c: '"#{htmlify(t2.data)}"'}), when[is_an_element(t1)]])]]}). Final configuration. This one loads all of the others (though it lets you specify whether you want indirected references or not). configuration('montenegro', function () {this.configure('montenegro.methods montenegro.events montenegro.fixes montenegro.rpc montenegro.dom')}); __ meta::sdoc('js::montenegro.server', <<'__'); Montenegro server library | Spencer Tipping Licensed under the terms of the MIT source code license Introduction. Montenegro extends Caterwaul (http://spencertipping.com/caterwaul) to operate in a node.js environment and provide an RPC endpoint. caterwaul. Node.js variables and Montenegro reference. Caterwaul has a problem with node.js variables. Specifically, code that it compiles can't reach the 'require' variable, which ends up being really important. To fix this, Montenegro binds that variable within any compiled function by using a macro. configuration('montenegro.core', function () {this.shallow('montenegro', {require: require})}). URL router. Montenegro gives you a quick proxy function to route requests based on URL patterns. This makes a suitable server if you want to promote it into one (and in fact it is the function you get back when you create a new server). Configuration is done like this: | var router = caterwaul.montenegro.route.url(); router.on('/foo', 'GET', fn[request, response][response /se[_.writeHead(200), _.end('bar')]]); router.not_found(request, response) = response /se[_.writeHead(404), _.end('Bummer dude, not found')]; router.on('/services', 'GET', router.service_listing); // Show a list of registered URLs Because routers provide the same interface they accept, you can nest them and create proxies. The last matching pattern is the one that handles the URL, so you can always refine URL matches (or override them) by adding new on() handlers. Each service built with this interface exposes a list of methods. You can enable it by using the server.service_listing method as the target of an 'on' invocation. You may not want to do this, but if exposing the request handlers is a security flaw then there is probably a larger problem with the design of the application. tconfiguration('std seq', 'montenegro.route.url', function () { this.configure('montenegro.core').montenegro /se[(_.route = _.route || {}) /se[ _.url() = l*[result(request, response) = result.route(request, response)] in result /se[_.handlers = seq[~[]], _.on(pattern, method, handler) = this /se[_.handlers.push({url: pattern, method: method, handler: handler})], _.route(request, response) = this /se[(_.handler_for(request.url, request.method) || _.not_found).call(_, request, response)], _.not_found(request, response) = response /se[_.writeHead(404), _.end('#{request.url} was not found.')], _.service_listing(req, res) = res /se.r[r.writeHead(200), r.end(seq[_.handlers *['#{_.url} (#{_.method})']].join('\n'))], _.handlers_for(url, method) = seq[this.handlers %[(_.url.test ? _.url.test(url) : _.url === url) && (! _.method || _.method === method)] *[_.handler]], _.handler_for(url, method) = this.handlers_for(url, method).pop()]]]}). Server construction. You construct a Montenegro server instance by calling montenegro.server(port). The server starts running immediately. Each server has an internal routing table that maps URL patterns to request handlers. (A request handler is just a function that Node's createServer would accept.) tconfiguration('std seq', 'montenegro.server', function () { l[require = this.configure('montenegro.core').montenegro.require] in this.configure('montenegro.route.url').montenegro /se[ _.server(port) = caterwaul.util.merge(_.route.url(), _.server.extensions) /se[require('http').createServer(_).listen(port || 8080, '0.0.0.0')], _.server.extensions = {}]}). Trivial HTML construction. This gives you a quick way to throw a page together. The key here is that you quote a syntax tree that will end up being executed on the client-side when jQuery loads. For example, to say hello world: | response /se[_.writeHead(200, {'content-type': 'text/html'}), _.end(montenegro.html(qs[$('body').append(html[h1('Hello world!')])]))]; This builds a client page that loads caterwaul.all.js, montenegro.client.js, and jQuery. By default, caterwaul.all.js and montenegro.client.js come from my webserver (which sometimes is down), but you can change where it requests these scripts by setting _.html.caterwaul_path, _.html.montenegro_path, and _.html.jquery_path. tconfiguration('std', 'montenegro.html', function () { this.configure('montenegro.core').montenegro /se[ _.html(t) = l*[html_header() = l[s(src) = ''] in '#{s(_.html.jquery_path)}#{s(_.html.caterwaul_path)}#{s(_.html.montenegro_path)}', wrap_initializer(s) = '', html_footer() = ''] in html_header() + wrap_initializer(qs[function () {return _t}].replace({_t: t}).serialize()) + html_footer(), _.html /se[_.caterwaul_path = 'http://spencertipping.com/caterwaul/caterwaul.all.js', _.montenegro_path = 'http://spencertipping.com/montenegro/montenegro.client.js', _.jquery_path = 'http://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.min.js']]}). RPC endpoints. You can create an RPC service on a URL. The RPC endpoint wraps the function in a CPS-converted HTTP request/response proxy that listens for POST requests on a given URL, expects a JSON array in the body, and converts the body into a list of parameters for the function you specify. Your function can access the reply continuation by either returning normally or invoking 'this' on the reply object. All listeners are CPS-converted, so you can have coroutine-based communication between the client and server. For example, this is a broadcast chat server (which relies on singly re-entrant continuations for replies, if you want to think about it as a regular procedure call): | var clients = seq[~[]]; caterwaul.montenegro.server(8080) /se[_.rpc('/chat', fn_[clients.push(this)]). rpc('/chat/send', fn[message][seq[clients *![_(message)]], clients = seq[~[]], this('OK')])]; The client code for this example is in montenegro.client.js.sdoc. RPC services can provide documentation. This is an optional second parameter, e.g: | chat_service.rpc('/chat', 'Clients should long-loop this URL to be notified of messages that are sent.', fn_[...]); Any clients who GET the URL will be served the documentation string as plain text. If you don't specify any documentation, GET requests will be sent a generic 'there's a service here, but no documentation for it' message as plain text. The service will also send potentially useful diagnostic messages with 400 error codes if you're using it incorrectly. tconfiguration('std continuation', 'montenegro.server.rpc', function () { l*[html = this.configure('montenegro.html').montenegro.html] in this.configure('montenegro.server').montenegro.server.extensions /se[ _.rpc(url, _documentation, _fn) = this /se.t[install_json_post_handler(t), install_documentation_handler(t), install_test_page(t), where*[documentation = _fn ? _documentation : '#{url} service (no documentation provided)', fn = _fn || _documentation, rpc = _.rpc, install_json_post_handler(server) = server.on(url, 'POST', fn[req, res][json_from(req)(fn[json][fn.apply(json_to(res), json)])]), install_documentation_handler(server) = server.on('#{url}/doc', 'GET', fn[req, res][res /se[_.writeHead(200, {'content-type': 'text/plain'}), _.end(doc)]]), install_test_page(server) = server.on(url, 'GET', fn[req, res][res /se[_.writeHead(200, {'content-type': 'text/html'}), _.end(rpc.testpage())]]), json_from(request)(cc) = l[pieces = []] in request /se[_.on('data', pieces/mb/push), _.on('end', fn_[unwind_protect[rpc.error(e)][cc(JSON.parse(pieces.join('')))]])], error_to (response)(e) = response /se[_.writeHead(400, {'content-type': 'text/plain'}), _.end(e.toString())], json_to (response)() = l[as = Array.prototype.slice.call(arguments)] in response /se[_.writeHead(200, {'content-type': 'application/json'}), _.end(JSON.stringify(as))]]], Error trapping. If an error occurs, the client receives the toString() produced by the error object and a stack trace is logged to the console. However, you may want to do something different. If you do, change montenegro.server.rpc.error(e). _.rpc.error(e) = e /se[console.log(_)], Test pages. If you use the server as shown above, you'll get a test page for each RPC endpoint. For example, the test page for the '/chat' URL is '/chat'. You can navigate to this page and send requests to the RPC to verify that it's working correctly. This is enabled in production-mode as well as development mode; it's my attempt to encode Kerckhoffs' principle (http://en.wikipedia.org/wiki/Kerckhoffs'_principle) into the framework to prevent bad security decisions. _.rpc.testpage() = html(qs[$('head').append(html[link *rel('stylesheet') *href('http://fonts.googleapis.com/css?family=Droid+Sans+Mono&subset=latin'), link *rel('stylesheet') *href('http://spencertipping.com/montenegro/style/testpage.css')]), $('body').append(html[div(div.header(h1('RPC shell'), h2.documentation(span.loading('loading documentation...'))), p('You can evaluate code below. ', code('rpc()'), ' is the RPC connector function for the API, and ', code('log()'), ' can be used to log values. Your code will be macroexpanded under std, seq, opt, parser, montenegro, and continuation.'), div(button.run('Run')), textarea.code /val('l/cps[x <- rpc("Hello world", _)][log(x)]'), div.log)]), window.rpc = caterwaul.montenegro.rpc(url), $('.run').click(fn_[unwind_protect[error(e)][caterwaul.clone('std seq continuation opt montenegro')('(function () {#{$("textarea.code").val()}})')()]]), $.get('#{url}/doc', fn[doc][$('.documentation').empty().append(doc)]), where*[entry(x) = html[div.entry(code(x), ' ', a('[x]')/click(fn_[$(this).parent().remove()]))], log = window.log(x) = $('div.log').append(entry(JSON.stringify(x))), error = window.error(x) = $('div.log').append(entry(x.toString()).addClass('error')), url = document.location.href]])]}). HTML server configuration. You can send HTML pages to the client by writing initialization functions. To send a hello world page, for example: | montenegro.server(8080).html('/hello', qs[$('body').append(html[h1('Hello world!')])]); The client file contains full documentation for the html[] macro (the client ends up macroexpanding the code above). tconfiguration('std', 'montenegro.server.html', function () { l[html = this.configure('montenegro.html').montenegro.html] in this.configure('montenegro.server').montenegro.server.extensions /se[ _.html(url, t) = l[s = html(t)] in this /se[_.on(url, 'GET', fn[req, res][res /se[_.writeHead(200, {'content-type': 'text/html'}), _.end(s)]])]]}). Proxy configuration. Forwards headers both ways, changing only the 'host' header for sending. You can specify functions to intercept the request/response data to transform it in some way. tconfiguration('std seq continuation', 'montenegro.server.proxy', function () { l[http = this.configure('montenegro.core').montenegro.require('http')] in this.configure('montenegro.server').montenegro.server.extensions /se[ _.proxy(url, options) = l/cps[(req, res) <- this.on(new RegExp('^#{url}'), null, _)] [l[req0 = proxy_request_for(req, url)] [req.pipe(req0), l/cps[res0 <- req0.on('response', _)] [res.writeHead(res0.statusCode, res0.headers), res0.pipe(res)]]], where*[parts_for(url) = /^\/?([^:\/]+)(:?\d*)(\/?.*)$/.exec(url), host_for(parts) = parts && parts[1], port_for(parts) = parts && Number(parts[2].substring(1)) || 80, proxy_request_for(req, base_url) = l*[parts = parts_for(req.url.replace(base_url, '')), host = host_for(parts), port = port_for(parts)] in http.createClient(port, host).request(req.method, parts && parts[3] || '/', caterwaul.util.merge({}, req.headers, {host: host}))]]}). File server configuration. Sometimes you want to serve files from a directory. This is a fairly simple service to do that. I imagine there are security problems with it. tconfiguration('std continuation', 'montenegro.server.file', function () { l[sanitize(s) = s.replace(/\.\+/g, '.'), fs = this.configure('montenegro.core').montenegro.require('fs')] in this.configure('montenegro.server').montenegro.server.extensions /se.e[ e.file_extension_mimetypes = {css: 'text/css', html: 'text/html', js: 'application/javascript', '': 'text/plain'}, e.file(url, filename) = this /se[l/cps[(req, res) <- this.on(new RegExp('^#{url.replace(/\/$/, "")}(/|$)'), 'GET', _)] [res.writeHead(200, {'content-type': content_type_for(new_url)}), read_stream.pipe(res), where*[new_url = req.url.replace(/\?.*$/, ''), read_stream = fs.createReadStream('#{filename}#{sanitize(new_url.substring(url.length))}'), content_type_for(url) = /\.(\w+)$/.exec(url) /re[_ && _[1] /re[e.file_extension_mimetypes[_] || e.file_extension_mimetypes['']]]]]]]}). Alias configuration. Gives you the ability to alias content with or without redirects. For example: | some_server.alias('/', '/index.html'); // A server-side redirect (no 30x return code) some_server.alias('/foo', '/bar', 'POST'); // Alias POST requests instead of everything some_server.alias(/^\/foo/(.*)$/, '/bar/$1'); // Alias all URLs starting with /foo/ to /bar/whatever some_server.redirect('/', '/index.html'); // A client-side redirect (301 error code) some_server.redirect('/', '/index.html', {code: 302}); // A client-side redirect with a custom code some_server.redirect('/foo', '/bar', {method: 'POST'}); // Issue redirect for POSTs instaed of GETs Specifying a method of null indicates that all methods should be aliased. tconfiguration('std continuation', 'montenegro.server.alias', function () { this.configure('montenegro.server').montenegro.server.extensions /se[ _.alias(from, to, method) = this /se[_.on(from, method, fn[req, res][_(req /se[_.url = from.test ? _.url.replace(from, to) : to], res)])], _.redirect(from, to, options) = this /se[l/cps[(req, res) <- this.on(from, options /re[_ && _.method], _)] in res /se[_.writeHead(options /re[_ && _.code] || 301, {location: to}), res.end()]]]}). Final configuration. This configuration bundles all of the configurations together. configuration('montenegro', function () {this.configure('montenegro.html montenegro.route.url montenegro.server montenegro.server.rpc montenegro.server.html montenegro.server.file', 'montenegro.server.alias montenegro.server.proxy')}); __ meta::sdoc('js::test/chat-server', <<'__'); A trivial chat server. caterwaul.clone('std seq montenegro')(function () { caterwaul.montenegro.server(8080) /se[ // Server code: _.rpc('/chat/listen', fn_[clients.push(this)]).rpc('/chat/send', fn[message][seq[clients *![_(message)]], clients = seq[~[]], this('OK')]), where[clients = seq[~[]]], // Client code: _.html('/', qs[caterwaul.montenegro.rpc('/chat/listen')(fn[message][$('.log').append(html[div.message(message)]), this()]), let*[send = caterwaul.montenegro.rpc('/chat/send')] in $('body').append(html[div.log(input /enter_key(fn_[send($(this).val()), $(this).val('')]))])])]})(); __ meta::sdoc('js::test/file', <<'__'); File server test app caterwaul.clone('std seq continuation montenegro')(function () { caterwaul.montenegro.server(8080) /se[_.file('/', './')]})(); __ meta::sdoc('js::test/paint-app', <<'__'); Collaborative paint test application caterwaul.clone('std seq montenegro')(function () { let[connections = seq[~[]]] in caterwaul.montenegro.server(8080) /se[ _.rpc('/listen', fn_[connections.push(this)]).rpc('/send', fn[message][seq[connections *![_(message)]], connections = seq[~[]], this('OK')]), _.html('/', qs[caterwaul.montenegro.rpc('/listen')(fn[message][this() /se[$('canvas')[0].getContext('2d') /se[_.moveTo(message.x1, message.y1), _.strokeStyle = message.color || '#35a', _.lineTo(message.x2, message.y2), _.stroke()]]]), let[send = caterwaul.montenegro.rpc('/send')] in $('body').append(let[x = 0, y = 0, adjust(e) = let[o = $('canvas').offset()] in e /se[_.real_x = _.pageX - o.left, _.real_y = _.pageY - o.top]] in html[canvas /css({border: 'solid 1px #888'}) /mousedown(fn[e][adjust(e), x = e.real_x, y = e.real_y]) /mousemove(fn[e][adjust(e), send({x1: x, y1: y, x2: x = e.real_x, y2: y = e.real_y}), when[x || y]]) /mouseup (fn[e][x = y = 0])])])]})(); __ meta::sdoc('js::test/proxy', <<'__'); Proxy test app caterwaul.clone('std seq continuation montenegro')(function () { caterwaul.montenegro.server(8080) /se[ _.proxy('/proxy/'), _.html('/', qs[$('body').append(html[input /val('www.google.com'), button('GET') /click(fn_[$.get('/proxy/#{$("input").val()}', fn[data][$('body').append(html[pre(data)])])])])])]})(); __ internal::main(); __END__