mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'shark' into devel
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template name="api">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h1 id="api">The Meteor API</h1>
|
||||
|
||||
@@ -1142,31 +1142,31 @@ applicable. If you only need to receive the fields that changed, see
|
||||
<dt><span class="name">added(document)</span> <span class="or">or</span></dt>
|
||||
<dt><span class="name">addedAt(document, atIndex, before)</span></dt>
|
||||
<dd>
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
A new document `document` entered the result set. The new document
|
||||
appears at position `atIndex`. It is immediately before the document
|
||||
whose `_id` is `before`. `before` will be `null` if the new document
|
||||
is at the end of the results.
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</dd>
|
||||
|
||||
<dt><span class="name">changed(newDocument, oldDocument)
|
||||
<span class="or">or</span></span></dt>
|
||||
<dt><span class="name">changedAt(newDocument, oldDocument, atIndex)</span></dt>
|
||||
<dd>
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
The contents of a document were previously `oldDocument` and are now
|
||||
`newDocument`. The position of the changed document is `atIndex`.
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</dd>
|
||||
|
||||
<dt><span class="name">removed(oldDocument)</span>
|
||||
<span class="or">or</span></dt>
|
||||
<dt><span class="name">removedAt(oldDocument, atIndex)</span></dt>
|
||||
<dd>
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
The document `oldDocument` is no longer in the result set. It used to be at position `atIndex`.
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</dd>
|
||||
|
||||
{{#dtdd "movedTo(document, fromIndex, toIndex, before)"}}
|
||||
@@ -1207,12 +1207,12 @@ result set, not the entire contents of the document that changed.
|
||||
<span class="or">or</span></dt>
|
||||
<dt><span class="name">addedBefore(id, fields, before)</span></dt>
|
||||
<dd>
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
A new document entered the result set. It has the `id` and `fields`
|
||||
specified. `fields` contains all fields of the document excluding the
|
||||
`_id` field. The new document is before the document identified by
|
||||
`before`, or at the end if `before` is `null`.
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</dd>
|
||||
|
||||
{{#dtdd "changed(id, fields)"}}
|
||||
@@ -1287,7 +1287,7 @@ that needs to deal with `_id` fields that may be either strings or `ObjectID`s,
|
||||
method, since Meteor currently constructs them fully randomly.
|
||||
{{/note}}
|
||||
|
||||
{{#api_box_inline selectors}}
|
||||
{{#api_box selectors}}
|
||||
|
||||
The simplest selectors are just a string or
|
||||
[`Meteor.Collection.ObjectID`](#collection_object_id). These selectors match the
|
||||
@@ -1319,9 +1319,9 @@ But they can also contain more complicated tests:
|
||||
See the [complete
|
||||
documentation](http://docs.mongodb.org/manual/reference/operator/).
|
||||
|
||||
{{/api_box_inline}}
|
||||
{{/api_box}}
|
||||
|
||||
{{#api_box_inline modifiers}}
|
||||
{{#api_box modifiers}}
|
||||
|
||||
A modifier is an object that describes how to update a document in
|
||||
place by changing some of its fields. Some examples:
|
||||
@@ -1344,9 +1344,9 @@ supported by [validated updates](#allow).)
|
||||
See the [full list of
|
||||
modifiers](http://www.mongodb.org/display/DOCS/Updating#Updating-ModifierOperations).
|
||||
|
||||
{{/api_box_inline}}
|
||||
{{/api_box}}
|
||||
|
||||
{{#api_box_inline sortspecifiers}}
|
||||
{{#api_box sortspecifiers}}
|
||||
|
||||
Sorts may be specified using your choice of several syntaxes:
|
||||
|
||||
@@ -1361,9 +1361,9 @@ The last form will only work if your JavaScript implementation
|
||||
preserves the order of keys in objects. Most do, most of the time, but
|
||||
it's up to you to be sure.
|
||||
|
||||
{{/api_box_inline}}
|
||||
{{/api_box}}
|
||||
|
||||
{{#api_box_inline fieldspecifiers}}
|
||||
{{#api_box fieldspecifiers}}
|
||||
|
||||
Queries can specify a particular set of fields to include or exclude from the
|
||||
result object.
|
||||
@@ -1406,7 +1406,7 @@ A more advanced example:
|
||||
See <a href="http://docs.mongodb.org/manual/tutorial/project-fields-from-query-results/#projection">
|
||||
the MongoDB docs</a> for details of the nested field rules and array behavior.
|
||||
|
||||
{{/api_box_inline}}
|
||||
{{/api_box}}
|
||||
|
||||
<h2 id="session"><span>Session</span></h2>
|
||||
|
||||
@@ -1419,7 +1419,7 @@ you call [`Session.get`](#session_get)`("currentList")`
|
||||
from inside a template, the template will automatically be rerendered
|
||||
whenever [`Session.set`](#session_set)`("currentList", x)` is called.
|
||||
|
||||
{{> api_box set}}
|
||||
{{> api_box session_set}}
|
||||
|
||||
Example:
|
||||
|
||||
@@ -1436,7 +1436,7 @@ Example:
|
||||
This is useful in initialization code, to avoid re-initializing a session
|
||||
variable every time a new version of your app is loaded.
|
||||
|
||||
{{> api_box get}}
|
||||
{{> api_box session_get}}
|
||||
|
||||
Example:
|
||||
|
||||
@@ -1464,7 +1464,7 @@ If value is a scalar, then these two expressions do the same thing:
|
||||
|
||||
Example:
|
||||
|
||||
<template name="postsView">
|
||||
{{lt}}template name="postsView">
|
||||
{{dstache}}! Show a dynamically updating list of items. Let the user click on an
|
||||
item to select it. The selected item is given a CSS class so it
|
||||
can be rendered differently. }}
|
||||
@@ -1472,11 +1472,11 @@ Example:
|
||||
{{dstache}}#each posts}}
|
||||
{{dstache}}> postItem }}
|
||||
{{dstache}}/each}}
|
||||
</{{! }}template>
|
||||
{{lt}}/template>
|
||||
|
||||
<template name="postItem">
|
||||
{{lt}}template name="postItem">
|
||||
<div class="{{dstache}}postClass}}">{{dstache}}title}}</div>
|
||||
</{{! }}template>
|
||||
{{lt}}/template>
|
||||
|
||||
///// in JS file
|
||||
Template.postsView.posts = function() {
|
||||
@@ -2011,9 +2011,9 @@ Example:
|
||||
|
||||
<h2 id="templates_api"><span>Templates</span></h2>
|
||||
|
||||
A template that you declare as `<{{! }}template name="foo"> ... </{{!
|
||||
}}template>` can be accessed as the function `Template.foo`, which
|
||||
returns a string of HTML when called.
|
||||
A template that you declare as `<{{! }}template name="foo"> ... <{{!
|
||||
}}/template>` can be accessed as the function `Template.foo`,
|
||||
which returns a string of HTML when called.
|
||||
|
||||
The same template may occur many times on the page, and these
|
||||
occurrences are called template instances. Template instances have a
|
||||
@@ -2239,88 +2239,8 @@ the template. It is updated each time the template is re-rendered.
|
||||
Access is read-only and non-reactive.
|
||||
|
||||
|
||||
{{> api_box render}}
|
||||
|
||||
`Meteor.render` creates a `DocumentFragment` (a sequence of DOM nodes)
|
||||
that automatically updates in realtime. Most Meteor apps don't need to
|
||||
call this directly; they use templates and Meteor handles the rendering.
|
||||
|
||||
Pass in `htmlFunc`, a function that returns an HTML
|
||||
string. `Meteor.render` calls the function and turns the output into
|
||||
DOM nodes. Meanwhile, it tracks the data that was used when `htmlFunc`
|
||||
ran, and automatically wires up callbacks so that whenever any of the
|
||||
data changes, `htmlFunc` is re-run and the DOM nodes are updated in
|
||||
place.
|
||||
|
||||
You may insert the returned `DocumentFragment` directly into the DOM
|
||||
wherever you would like it to appear. The inserted nodes will continue
|
||||
to update until they are taken off the screen. Then they will be
|
||||
automatically cleaned up. For more details about clean-up, see
|
||||
[`Deps.flush`](#deps_flush).
|
||||
|
||||
`Meteor.render` tracks the data dependencies of `htmlFunc` by running
|
||||
it in a reactive computation, so it can respond to changes in any reactive
|
||||
data sources used by that function. For more information, or to learn
|
||||
how to make your own reactive data sources, see
|
||||
[Reactivity](#reactivity).
|
||||
|
||||
Example:
|
||||
|
||||
// Client side: show the number of players online.
|
||||
var frag = Meteor.render(function () {
|
||||
return "<p>There are " + Players.find({online: true}).count() +
|
||||
" players online.</p>";
|
||||
});
|
||||
document.body.appendChild(frag);
|
||||
|
||||
// Server side: find all players that have been idle for a while,
|
||||
// and mark them as offline. The count on the screen will
|
||||
// automatically update on all clients.
|
||||
Players.update({idleTime: {$gt: 30}}, {$set: {online: false}});
|
||||
|
||||
{{> api_box renderList}}
|
||||
|
||||
Creates a `DocumentFragment` that automatically updates as the results
|
||||
of a database query change. Most Meteor apps use `{{dstache}}#each}}` in
|
||||
a template instead of calling this directly.
|
||||
|
||||
`renderList` is more efficient than using `Meteor.render` to render HTML
|
||||
for a list of documents. For example, if a new document is created in
|
||||
the database that matches the query, a new item will be rendered and
|
||||
inserted at the appropriate place in the DOM without re-rendering the
|
||||
other elements. Similarly, if a document changes position in a sorted
|
||||
query, the DOM nodes will simply be moved and not re-rendered.
|
||||
|
||||
`docFunc` is called as needed to generate HTML for each document. If
|
||||
you provide `elseFunc`, then whenever the query returns no results, it
|
||||
will be called to render alternate content. You might use this to show
|
||||
a message like "No records match your query."
|
||||
|
||||
Each call to `docFunc` or `elseFunc` is run in its own reactive
|
||||
computation so that if it has other external data dependencies, it will be
|
||||
individually re-run when the data changes.
|
||||
|
||||
Example:
|
||||
|
||||
// List the titles of all of the posts that have the tag
|
||||
// "frontpage". Keep the list updated as new posts are made, as tags
|
||||
// change, etc. Display the selected post differently.
|
||||
var frag = Meteor.renderList(
|
||||
Posts.find({tags: "frontpage"}),
|
||||
function(post) {
|
||||
var style = Session.equals("selectedId", post._id) ? "selected" : "";
|
||||
// A real app would need to quote/sanitize post.name
|
||||
return '<div class="' + style + '">' + post.name + '</div>';
|
||||
});
|
||||
document.body.appendChild(frag);
|
||||
|
||||
// Select a post. This will cause only the selected item and the
|
||||
// previously selected item to update.
|
||||
var somePost = Posts.findOne({tags: "frontpage"});
|
||||
Session.set("selectedId", somePost._id);
|
||||
|
||||
|
||||
{{#api_box_inline eventmaps}}
|
||||
{{#api_box eventmaps}}
|
||||
|
||||
Several functions take event maps. An event map is an object where
|
||||
the properties specify a set of events to handle, and the values are
|
||||
@@ -2492,11 +2412,11 @@ Other DOM events are available as well, but for the events above,
|
||||
Meteor has taken some care to ensure that they work uniformly in all
|
||||
browsers.
|
||||
|
||||
{{/api_box_inline}}
|
||||
{{/api_box}}
|
||||
|
||||
|
||||
|
||||
{{#api_box_inline constant}}
|
||||
{{#api_box constant}}
|
||||
|
||||
You can mark a region of a template as "constant" and not subject to
|
||||
re-rendering using the
|
||||
@@ -2525,9 +2445,9 @@ correctly inside constant regions.
|
||||
{{/note}}
|
||||
|
||||
|
||||
{{/api_box_inline}}
|
||||
{{/api_box}}
|
||||
|
||||
{{#api_box_inline isolate}}
|
||||
{{#api_box isolate}}
|
||||
|
||||
Each template runs as its own reactive computation. When the template
|
||||
accesses a reactive data source, such as by calling `Session.get` or
|
||||
@@ -2545,7 +2465,7 @@ re-rendered. This block helper essentially conveys the reactivity
|
||||
benefits you would get by pulling the content out into a new
|
||||
sub-template.
|
||||
|
||||
{{/api_box_inline}}
|
||||
{{/api_box}}
|
||||
|
||||
<h2 id="match"><span>Match</span></h2>
|
||||
|
||||
@@ -2581,7 +2501,7 @@ server logs but not revealed to the client.
|
||||
|
||||
{{> api_box match_test}}
|
||||
|
||||
{{#api_box_inline matchpatterns}}
|
||||
{{#api_box matchpatterns}}
|
||||
|
||||
The following patterns can be used as pattern arguments to `check` and `Match.test`:
|
||||
|
||||
@@ -2661,7 +2581,7 @@ from the call to `check` or `Match.test`. Examples:
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
{{/api_box_inline}}
|
||||
{{/api_box}}
|
||||
|
||||
<h2 id="timers"><span>Timers</span></h2>
|
||||
|
||||
@@ -3243,11 +3163,11 @@ by spammers.)
|
||||
'Hello from Meteor!',
|
||||
'This is a test of Email.send.');
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
|
||||
<h2 id="assets"><span>Assets</span></h2>
|
||||
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
`Assets` allows server code in a Meteor application to access static server
|
||||
assets, which are located in the `private` subdirectory of an application's
|
||||
tree.
|
||||
@@ -3261,7 +3181,7 @@ directory called `nested` with a file called `data.txt` inside it, then server
|
||||
code can read `data.txt` by running:
|
||||
|
||||
var data = Assets.getText('nested/data.txt');
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
|
||||
</template>
|
||||
|
||||
@@ -3272,6 +3192,7 @@ code can read `data.txt` by running:
|
||||
|
||||
<template name="api_box">
|
||||
<div class="api {{bare}}">
|
||||
|
||||
<h3 id="{{id}}">
|
||||
<a class="name selflink" href="#{{id}}">{{{name}}}</a>
|
||||
{{#if locus}}
|
||||
@@ -3280,7 +3201,7 @@ code can read `data.txt` by running:
|
||||
</h3>
|
||||
|
||||
<div class="desc">
|
||||
{{#each descr}}{{#better_markdown}}{{{this}}}{{/better_markdown}}{{/each}}
|
||||
{{#each descr}}{{#markdown}}{{{this}}}{{/markdown}}{{/each}}
|
||||
</div>
|
||||
|
||||
{{#if args}}
|
||||
@@ -3293,8 +3214,8 @@ code can read `data.txt` by running:
|
||||
{{> api_box_args options}}
|
||||
{{/if}}
|
||||
|
||||
{{#if body}}
|
||||
{{#better_markdown}}{{{body}}}{{/better_markdown}}
|
||||
{{#if UI.contentBlock}}
|
||||
{{#markdown}}{{> UI.contentBlock}}{{/markdown}}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
@@ -3314,12 +3235,12 @@ code can read `data.txt` by running:
|
||||
{{{type}}}
|
||||
{{/if}}
|
||||
</span></dt>
|
||||
<dd>{{#better_markdown}}{{{descr}}}{{/better_markdown}}</dd>
|
||||
<dd>{{#markdown}}{{{descr}}}{{/markdown}}</dd>
|
||||
{{/each}}
|
||||
</dl>
|
||||
</template>
|
||||
|
||||
|
||||
<template name="api_section_helper">
|
||||
<template name="api_section">
|
||||
<h2 id="{{id}}"><a href="#{{id}}" class="selflink"><span>{{name}}</span></a></h2>
|
||||
</template>
|
||||
|
||||
@@ -805,7 +805,7 @@ Template.api.cursor_observe_changes = {
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.id = {
|
||||
Template.api.random_id = {
|
||||
id: "meteor_id",
|
||||
name: "Random.id()",
|
||||
locus: "Anywhere",
|
||||
@@ -994,37 +994,6 @@ Template.api.dependency_hasdependents = {
|
||||
// writeFence
|
||||
// invalidationCrossbar
|
||||
|
||||
Template.api.render = {
|
||||
id: "meteor_render",
|
||||
name: "Meteor.render(htmlFunc)",
|
||||
locus: "Client",
|
||||
descr: ["Create DOM nodes that automatically update themselves as data changes."],
|
||||
args: [
|
||||
{name: "htmlFunc",
|
||||
type: "Function returning a string of HTML",
|
||||
descr: "Function that generates HTML to be rendered. Called immediately and re-run whenever data changes. May also be a string of HTML instead of a function."}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.renderList = {
|
||||
id: "meteor_renderlist",
|
||||
name: "Meteor.renderList(observable, docFunc, [elseFunc])",
|
||||
locus: "Client",
|
||||
descr: ["Create DOM nodes that automatically update themselves based on the results of a database query."],
|
||||
args: [
|
||||
{name: "observable",
|
||||
type: "Cursor",
|
||||
type_link: "meteor_collection_cursor",
|
||||
descr: "Query cursor to observe as a reactive source of ordered documents."},
|
||||
{name: "docFunc",
|
||||
type: "Function taking a document and returning HTML",
|
||||
descr: "Render function to be called for each document."},
|
||||
{name: "elseFunc",
|
||||
type: "Function returning HTML",
|
||||
descr: "Optional. Render function to be called when query is empty."}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
Template.api.eventmaps = {
|
||||
id: "eventmaps",
|
||||
@@ -1653,7 +1622,8 @@ Template.api.bindEnvironment = {
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.set = {
|
||||
// Can't name this '.set', since that's a method on components.
|
||||
Template.api.session_set = {
|
||||
id: "session_set",
|
||||
name: "Session.set(key, value)",
|
||||
locus: "Client",
|
||||
@@ -1683,7 +1653,8 @@ Template.api.setDefault = {
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.get = {
|
||||
// Can't name this '.get', since that's a method on components.
|
||||
Template.api.session_get = {
|
||||
id: "session_get",
|
||||
name: "Session.get(key)",
|
||||
locus: "Client",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template name="commandline">
|
||||
<div>
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
{{#api_section "commandline"}}Command line{{/api_section}}
|
||||
|
||||
@@ -188,6 +188,6 @@ instead of deploying to Meteor's servers. You will have to deal with
|
||||
logging, monitoring, backups, load-balancing, etc, all of which we
|
||||
handle for you if you use `meteor deploy`.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,7 +21,7 @@ when writing those apps.
|
||||
|
||||
<template name="whatismeteor">
|
||||
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h2 id="whatismeteor">What is Meteor?</h2>
|
||||
|
||||
@@ -69,11 +69,11 @@ In Meteor 1.0, the `meteor` build tool will have full support for
|
||||
Atmosphere.
|
||||
{{/note}}
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
<template name="structure">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h2 id="structuringyourapp">Structuring your application</h2>
|
||||
|
||||
@@ -167,11 +167,11 @@ application are loaded according to these rules:
|
||||
* Finally, all files that match `main.*` are moved after everything else
|
||||
(preserving their order).
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
<template name="data">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h2 id="dataandsecurity">Data and security</h2>
|
||||
|
||||
@@ -347,11 +347,11 @@ and publish functions validate all of their arguments. Just run
|
||||
method or publish function which skips `check`ing any of its arguments will fail
|
||||
with an exception.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
<template name="reactivity">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h2 id="reactivity">Reactivity</h2>
|
||||
|
||||
@@ -414,11 +414,11 @@ Meteor's
|
||||
is a package called [`Deps`](#deps) that is fairly short and straightforward.
|
||||
You can use it yourself to implement new reactive data sources.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
<template name="livehtml">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h2 id="livehtml">Live HTML</h2>
|
||||
|
||||
@@ -485,11 +485,11 @@ preserve these elements even when their enclosing template is
|
||||
rerendered, but will still update their children and copy over any
|
||||
attribute changes.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
<template name="templates">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h2 id="templates">Templates</h2>
|
||||
|
||||
@@ -518,7 +518,7 @@ function `Template.hello`, passing any data for the template:
|
||||
<!-- in myapp.html -->
|
||||
<template name="hello">
|
||||
<div class="greeting">Hello there, {{dstache}}first}} {{dstache}}last}}!</div>
|
||||
</{{! }}template>
|
||||
{{lt}}/template>
|
||||
|
||||
// in the JavaScript console
|
||||
> Template.hello({first: "Alyssa", last: "Hacker"});
|
||||
@@ -541,7 +541,7 @@ functions in JavaScript. Just add the helper functions directly on the
|
||||
{{dstache}}#each topScorers}}
|
||||
<div>{{dstache}}name}}</div>
|
||||
{{dstache}}/each}}
|
||||
</{{! }}template>
|
||||
{{lt}}/template>
|
||||
|
||||
instead of passing in `topScorers` as data when we call the
|
||||
template function, we could define a function on `Template.players`:
|
||||
@@ -574,7 +574,7 @@ in `this`. Note that some block helpers change the current context (notably
|
||||
<div>Senior: {{dstache}}name}}</div>
|
||||
{{dstache}}/if}}
|
||||
{{dstache}}/each}}
|
||||
</{{! }}template>
|
||||
{{lt}}/template>
|
||||
|
||||
{{#note}}
|
||||
Handlebars note: `{{dstache}}#if leagueIs "junior"}}` is
|
||||
@@ -599,13 +599,13 @@ the data context of the element that triggered the event.
|
||||
{{dstache}}#each player}}
|
||||
{{dstache}}> playerScore}}
|
||||
{{dstache}}/each}}
|
||||
</{{! }}template>
|
||||
{{lt}}/template>
|
||||
|
||||
<template name="playerScore">
|
||||
<div>{{dstache}}name}}: {{dstache}}score}}
|
||||
<span class="givePoints">Give points</span>
|
||||
</div>
|
||||
</{{! }}template>
|
||||
{{lt}}/template>
|
||||
|
||||
<!-- myapp.js -->
|
||||
Template.playerScore.events({
|
||||
@@ -622,7 +622,7 @@ discussion.
|
||||
<!-- in myapp.html -->
|
||||
<template name="forecast">
|
||||
<div>It'll be {{dstache}}prediction}} tonight</div>
|
||||
</{{! }}template>
|
||||
{{lt}}/template>
|
||||
|
||||
<!-- in myapp.js -->
|
||||
// JavaScript: reactive helper function
|
||||
@@ -638,11 +638,11 @@ discussion.
|
||||
> Session.set("weather", "cool and dry");
|
||||
In DOM: <div>It'll be cool and dry tonight</div>
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
<template name="packages_concept">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h2 id="usingpackages">Using packages</h2>
|
||||
|
||||
@@ -679,12 +679,12 @@ are willing to brave the fact that the Meteor package format is not
|
||||
documented yet and will change significantly before Meteor 1.0. See
|
||||
[Writing Packages](#writingpackages).
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
|
||||
<template name="namespacing">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h2 id="namespacing">Namespacing</h2>
|
||||
|
||||
@@ -773,12 +773,12 @@ certainly shouldn't depend on this quirk, and in the future Meteor may
|
||||
check for it and throw an error if you do.
|
||||
{{/note}}
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
|
||||
<template name="deploying">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h2 id="deploying">Deploying</h2>
|
||||
|
||||
@@ -845,12 +845,12 @@ have `npm` available, and run the following:
|
||||
$ npm install fibers@1.0.1
|
||||
{{/warning}}
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
|
||||
<template name="packages_writing">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h2 id="writingpackages">Writing packages</h2>
|
||||
|
||||
@@ -949,5 +949,5 @@ quick tips:
|
||||
machine architecture, but if not your built Meteor package will be
|
||||
portable.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -23,18 +23,41 @@
|
||||
</body>
|
||||
|
||||
<template name="nav">
|
||||
<div id="nav-inner">
|
||||
{{#each sections}}
|
||||
{{#if type "spacer"}}
|
||||
<div class="spacer"> </div>
|
||||
<div id="nav-inner">
|
||||
{{#each sections}}
|
||||
{{#if type "spacer"}}
|
||||
<div class="spacer"> </div>
|
||||
{{/if}}
|
||||
{{#if type "section"}}
|
||||
{{#nav_section}}
|
||||
<a href="#{{id}}" class="{{maybe_current}} {{style}}">
|
||||
{{#if prefix}}{{prefix}}.{{/if}}{{#if instance}}<i>{{instance}}</i>.{{/if}}{{name}}
|
||||
</a>
|
||||
{{/nav_section}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="nav_section">
|
||||
{{#if depthIs 1}}
|
||||
<h1>{{> UI.contentBlock}}</h1>
|
||||
{{/if}}
|
||||
{{#if type "section"}}
|
||||
<h{{depth}}><a href="#{{id}}" class="{{maybe_current}} {{style}}">
|
||||
{{#if prefix}}{{prefix}}.{{/if}}{{#if instance}}<i>{{instance}}</i>.{{/if}}{{name}}
|
||||
</a></h{{depth}}>
|
||||
{{#if depthIs 2}}
|
||||
<h2>{{> UI.contentBlock}}</h2>
|
||||
{{/if}}
|
||||
{{#if depthIs 3}}
|
||||
<h3>{{> UI.contentBlock}}</h3>
|
||||
{{/if}}
|
||||
{{#if depthIs 4}}
|
||||
<h4>{{> UI.contentBlock}}</h4>
|
||||
{{/if}}
|
||||
{{#if depthIs 5}}
|
||||
<h5>{{> UI.contentBlock}}</h5>
|
||||
{{/if}}
|
||||
{{#if depthIs 6}}
|
||||
<h6>{{> UI.contentBlock}}</h6>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- This only is displayed on narrow displays (eg phone) -->
|
||||
@@ -43,24 +66,35 @@
|
||||
</template>
|
||||
|
||||
|
||||
<template name="dtdd_helper">
|
||||
<dt><span class="name">{{{name}}}</span>
|
||||
{{#if type}}<span class="type">{{type}}</span>{{/if}}
|
||||
</dt>
|
||||
<dd>{{#better_markdown}}{{{descr}}}{{/better_markdown}}</dd>
|
||||
<template name="dtdd">
|
||||
{{! can be used with either one argument (which ends up in the data
|
||||
context, or with both name and type }}
|
||||
|
||||
{{#if name}}
|
||||
<dt><span class="name">{{{name}}}</span>
|
||||
{{#if type}}<span class="type">{{type}}</span>{{/if}}
|
||||
</dt>
|
||||
{{else}}
|
||||
<dt><span class="name">{{{.}}}</span></dt>
|
||||
{{/if}}
|
||||
<dd>{{#markdown}}{{> UI.contentBlock}}{{/markdown}}</dd>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<template name="warning_helper">
|
||||
<div class="warning">
|
||||
{{#better_markdown}}{{{this}}}{{/better_markdown}}
|
||||
</div>
|
||||
<!-- this is used within {{#markdown}}. <div> must be unindented.
|
||||
http://daringfireball.net/projects/markdown/syntax#html -->
|
||||
<template name="warning">
|
||||
<div class="warning">
|
||||
{{#markdown}}{{> UI.contentBlock}}{{/markdown}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="note_helper">
|
||||
<div class="note">
|
||||
{{#better_markdown}}{{{this}}}{{/better_markdown}}
|
||||
</div>
|
||||
<!-- this is used within {{#markdown}}. <div> must be unindented.
|
||||
http://daringfireball.net/projects/markdown/syntax#html -->
|
||||
<template name="note">
|
||||
<div class="note">
|
||||
{{#markdown}}{{> UI.contentBlock}}{{/markdown}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ Template.headline.release = function () {
|
||||
return Meteor.release || "(checkout)";
|
||||
};
|
||||
|
||||
|
||||
Meteor.startup(function () {
|
||||
// XXX this is broken by the new multi-page layout. Also, it was
|
||||
// broken before the multi-page layout because it had illegible
|
||||
@@ -248,8 +247,6 @@ var toc = [
|
||||
{instance: "this", name: "lastNode", id: "template_lastNode"},
|
||||
{instance: "this", name: "data", id: "template_data"}
|
||||
],
|
||||
"Meteor.render",
|
||||
"Meteor.renderList",
|
||||
{type: "spacer"},
|
||||
{name: "Event maps", style: "noncode"},
|
||||
{name: "Constant regions", style: "noncode", id: "constant"},
|
||||
@@ -407,153 +404,20 @@ Template.nav.maybe_current = function () {
|
||||
return Session.equals("section", this.id) ? "current" : "";
|
||||
};
|
||||
|
||||
Handlebars.registerHelper('warning', function(fn) {
|
||||
return Template.warning_helper(fn(this));
|
||||
});
|
||||
Template.nav_section.depthIs = function (n) {
|
||||
return this.depth === n;
|
||||
};
|
||||
|
||||
Handlebars.registerHelper('note', function(fn) {
|
||||
return Template.note_helper(fn(this));
|
||||
});
|
||||
|
||||
// "name" argument may be provided as part of options.hash instead.
|
||||
Handlebars.registerHelper('dtdd', function(name, options) {
|
||||
if (options && options.hash) {
|
||||
// {{#dtdd name}}
|
||||
options.hash.name = name;
|
||||
} else {
|
||||
// {{#dtdd name="foo" type="bar"}}
|
||||
options = name;
|
||||
}
|
||||
|
||||
return Template.dtdd_helper({descr: options.fn(this),
|
||||
name: options.hash.name,
|
||||
type: options.hash.type});
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('better_markdown', function(fn) {
|
||||
var converter = new Showdown.converter();
|
||||
var input = fn(this);
|
||||
|
||||
///////
|
||||
// Make Markdown *actually* skip over block-level elements when
|
||||
// processing a string.
|
||||
//
|
||||
// Official Markdown doesn't descend into
|
||||
// block elements written out as HTML (divs, tables, etc.), BUT
|
||||
// it doesn't skip them properly either. It assumes they are
|
||||
// either pretty-printed with their contents indented, or, failing
|
||||
// that, it just scans for a close tag with the same name, and takes
|
||||
// it regardless of whether it is the right one. As a hack to work
|
||||
// around Markdown's hacks, we find the block-level elements
|
||||
// using a proper recursive method and rewrite them to be indented
|
||||
// with the final close tag on its own line.
|
||||
///////
|
||||
|
||||
// Open-block tag should be at beginning of line,
|
||||
// and not, say, in a string literal in example code, or in a pre block.
|
||||
// Tag must be followed by a non-word-char so that we match whole tag, not
|
||||
// eg P for PRE. All regexes we wish to use when scanning must have
|
||||
// 'g' flag so that they respect (and set) lastIndex.
|
||||
// Assume all tags are lowercase.
|
||||
var rOpenBlockTag = /^\s{0,2}<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)(?=\W)/mg;
|
||||
var rTag = /<(\/?\w+)/g;
|
||||
var idx = 0;
|
||||
var newParts = [];
|
||||
var blockBuf = [];
|
||||
// helper function to execute regex `r` starting at idx and putting
|
||||
// the end index back into idx; accumulate the intervening string
|
||||
// into an array; and return the regex's first capturing group.
|
||||
var rcall = function(r, inBlock) {
|
||||
var lastIndex = idx;
|
||||
r.lastIndex = lastIndex;
|
||||
var match = r.exec(input);
|
||||
var result = null;
|
||||
if (! match) {
|
||||
idx = input.length;
|
||||
} else {
|
||||
idx = r.lastIndex;
|
||||
result = match[1];
|
||||
}
|
||||
(inBlock ? blockBuf : newParts).push(input.substring(lastIndex, idx));
|
||||
return result;
|
||||
};
|
||||
|
||||
// This is a tower of terrible hacks.
|
||||
// Replace Spark annotations <$...> ... </$...> with HTML comments, and
|
||||
// space out the comments on their own lines. This keeps them from
|
||||
// interfering with Markdown's paragraph parsing.
|
||||
// Really, running Markdown multiple times on the same string is just a
|
||||
// bad idea.
|
||||
input = input.replace(/<(\/?\$.*?)>/g, '<!--$1-->');
|
||||
input = input.replace(/<!--.*?-->/g, '\n\n$&\n\n');
|
||||
|
||||
var hashedBlocks = {};
|
||||
var numHashedBlocks = 0;
|
||||
|
||||
var nestedTags = [];
|
||||
while (idx < input.length) {
|
||||
var blockTag = rcall(rOpenBlockTag, false);
|
||||
if (blockTag) {
|
||||
nestedTags.push(blockTag);
|
||||
while (nestedTags.length) {
|
||||
var tag = rcall(rTag, true);
|
||||
if (! tag) {
|
||||
throw new Error("Expected </"+nestedTags[nestedTags.length-1]+
|
||||
"> but found end of string");
|
||||
} else if (tag.charAt(0) === '/') {
|
||||
// close tag
|
||||
var tagToPop = tag.substring(1);
|
||||
var tagPopped = nestedTags.pop();
|
||||
if (tagPopped !== tagToPop)
|
||||
throw new Error(("Mismatched close tag, expected </"+tagPopped+
|
||||
"> but found </"+tagToPop+">: "+
|
||||
input.substr(idx-50,50)+"{HERE}"+
|
||||
input.substr(idx,50)).replace(/\n/g,'\\n'));
|
||||
} else {
|
||||
// open tag
|
||||
nestedTags.push(tag);
|
||||
}
|
||||
}
|
||||
var newBlock = blockBuf.join('');
|
||||
var openTagFinish = newBlock.indexOf('>') + 1;
|
||||
var closeTagLoc = newBlock.lastIndexOf('<');
|
||||
|
||||
var key = ++numHashedBlocks;
|
||||
hashedBlocks[key] = newBlock.slice(openTagFinish, closeTagLoc);
|
||||
newParts.push(newBlock.slice(0, openTagFinish),
|
||||
'!!!!HTML:'+key+'!!!!',
|
||||
newBlock.slice(closeTagLoc));
|
||||
blockBuf.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var newInput = newParts.join('');
|
||||
var output = converter.makeHtml(newInput);
|
||||
|
||||
output = output.replace(/!!!!HTML:(.*?)!!!!/g, function(z, a) {
|
||||
return hashedBlocks[a];
|
||||
});
|
||||
|
||||
output = output.replace(/<!--(\/?\$.*?)-->/g, '<$1>');
|
||||
|
||||
return output;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('dstache', function() {
|
||||
UI.registerHelper('dstache', function() {
|
||||
return '{{';
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('tstache', function() {
|
||||
UI.registerHelper('tstache', function() {
|
||||
return '{{{';
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('api_section', function(id, nameFn) {
|
||||
return Template.api_section_helper(
|
||||
{name: nameFn(this), id:id}, true);
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('api_box_inline', function(box, fn) {
|
||||
return Template.api_box(_.extend(box, {body: fn(this)}), true);
|
||||
UI.registerHelper('lt', function () {
|
||||
return '<';
|
||||
});
|
||||
|
||||
Template.api_box.bare = function() {
|
||||
@@ -562,7 +426,7 @@ Template.api_box.bare = function() {
|
||||
(this.options && this.options.length)) ? "" : "bareapi";
|
||||
};
|
||||
|
||||
var check_links = function() {
|
||||
check_links = function() {
|
||||
var body = document.body.innerHTML;
|
||||
|
||||
var id_set = {};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="packages">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
<h1 id="packages">Packages</h1>
|
||||
|
||||
@@ -34,5 +34,5 @@ and removed with:
|
||||
{{> pkg_showdown}}
|
||||
{{> pkg_underscore}}
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_accounts_ui">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `accounts-ui`
|
||||
|
||||
A turn-key user interface for Meteor Accounts.
|
||||
@@ -29,5 +29,5 @@ when the URLs are loaded.
|
||||
|
||||
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_amplify">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
## `amplify`
|
||||
|
||||
@@ -10,5 +10,5 @@ components, and several useful utility functions.
|
||||
Amplify defines a global namespace `amplify` on the client only. It does
|
||||
not run on the server.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_appcache">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
## `appcache`
|
||||
|
||||
@@ -89,5 +89,5 @@ cache, see the
|
||||
[AppCache page](https://github.com/meteor/meteor/wiki/AppCache)
|
||||
in the Meteor wiki.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_audit_argument_checks">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
## `audit-argument-checks`
|
||||
|
||||
@@ -14,5 +14,5 @@ Methods and publish functions that do not need to validate their arguments can
|
||||
simply run `check(arguments, [Match.Any])` to satisfy the
|
||||
`audit-argument-checks` coverage checker.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_backbone">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `backbone`
|
||||
|
||||
[Backbone](http://documentcloud.github.com/backbone/) is a popular client-side MVC framework for managing complex
|
||||
@@ -7,5 +7,5 @@ data in the browser. In addition to the MVC and DOM-binding
|
||||
functionality, it also provides an API for HTML5 pushState and
|
||||
client-side URL routing.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_bootstrap">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `bootstrap`
|
||||
|
||||
[Twitter's Bootstrap](http://twitter.github.com/bootstrap/) package is a front-end toolkit for faster, more
|
||||
@@ -9,5 +9,5 @@ interactions including typography, forms, buttons, tables, grids, and
|
||||
navigation.
|
||||
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_browser_policy">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `browser-policy`
|
||||
|
||||
The `browser-policy` package lets you set security-related policies that will be
|
||||
@@ -170,5 +170,5 @@ sites can frame your site, while
|
||||
sites can be loaded inside frames on your site.
|
||||
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_coffeescript">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `coffeescript`
|
||||
|
||||
[CoffeeScript](http://coffeescript.org/) is a little language that
|
||||
@@ -48,5 +48,5 @@ Heavy CoffeeScript users, please let us know how this arrangement
|
||||
works for you, whether `share` is helpful for you, and anything else
|
||||
you'd like to see changed.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_d3">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
## `d3`
|
||||
|
||||
@@ -13,5 +13,5 @@ to DOM manipulation.
|
||||
The `d3` package adds the D3 library to the client JavaScript
|
||||
bundle. It has no effect on the server.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_force_ssl">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `force-ssl`
|
||||
|
||||
This package causes Meteor to redirect insecure connections (HTTP) to a
|
||||
@@ -19,5 +19,5 @@ Applications deployed to `meteor.com` subdomains with
|
||||
`meteor deploy` are automatically served via HTTPS using Meteor's
|
||||
certificate.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_jquery">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
|
||||
## `jquery`
|
||||
|
||||
@@ -17,5 +17,5 @@ plugins as separate packages. These include:
|
||||
* [`jquery-layout`](http://layout.jquery-dev.net/)
|
||||
* [`jquery-waypoints`](http://imakewebthings.com/jquery-waypoints/)
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_less">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `less`
|
||||
|
||||
[LESS](http://lesscss.org/) extends CSS with dynamic behavior such as variables, mixins,
|
||||
@@ -15,5 +15,5 @@ If you want to `@import` a file, give it the extension `.import.less`
|
||||
to prevent Meteor from processing it independently.
|
||||
{{/note}}
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_random">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `random`
|
||||
|
||||
The `random` package provides several functions for generating random
|
||||
@@ -28,5 +28,5 @@ Returns a random string of `n` hexadecimal digits.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_spiderable">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `spiderable`
|
||||
|
||||
|
||||
@@ -34,5 +34,5 @@ If you deploy your application with `meteor bundle`, you must install
|
||||
{{/warning}}
|
||||
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_stylus">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `stylus`
|
||||
|
||||
[Stylus](http://learnboost.github.com/stylus/) is a CSS pre-processor with a simple syntax and expressive
|
||||
@@ -21,5 +21,5 @@ to prevent Meteor from processing it independently.
|
||||
|
||||
See <http://visionmedia.github.com/nib> for documentation of the nib extensions of Stylus.
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template name="pkg_underscore">
|
||||
{{#better_markdown}}
|
||||
{{#markdown}}
|
||||
## `underscore`
|
||||
|
||||
[Underscore](http://underscorejs.org/) is a utility-belt library for
|
||||
@@ -30,5 +30,5 @@ are treated as objects if they have no prototype (specifically, if
|
||||
{{/warning}}
|
||||
|
||||
|
||||
{{/better_markdown}}
|
||||
{{/markdown}}
|
||||
</template>
|
||||
|
||||
1
examples/clock/.meteor/.gitignore
vendored
Normal file
1
examples/clock/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
9
examples/clock/.meteor/packages
Normal file
9
examples/clock/.meteor/packages
Normal file
@@ -0,0 +1,9 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
standard-app-packages
|
||||
autopublish
|
||||
insecure
|
||||
underscore
|
||||
1
examples/clock/.meteor/release
Normal file
1
examples/clock/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
none
|
||||
37
examples/clock/client/clock.html
Normal file
37
examples/clock/client/clock.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<head>
|
||||
<title>SVG Clock Demo</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="400" height="400"
|
||||
viewBox="-110 -110 220 220">
|
||||
|
||||
<!-- bounding circle -->
|
||||
<circle style="stroke: black; fill: #eee;"
|
||||
cx="0" cy="0" r="100"/>
|
||||
|
||||
<!-- hour, minute and second hands -->
|
||||
{{#with handData}}
|
||||
<line {{radial hourDegrees 0 .55}}
|
||||
style="stroke-width: 6px;
|
||||
stroke: green;" />
|
||||
<line {{radial minuteDegrees 0 .85}}
|
||||
style="stroke-width: 4px;
|
||||
stroke: blue;" />
|
||||
<line {{radial secondDegrees 0 .95}}
|
||||
style="stroke-width: 2px;
|
||||
stroke: red;" />
|
||||
{{/with}}
|
||||
|
||||
<!-- tick marks -->
|
||||
{{#each hours}}
|
||||
<line {{radial degrees 0.9 1}}
|
||||
style="stroke-width: 3px;
|
||||
stroke: black;" />
|
||||
{{/each}}
|
||||
</svg>
|
||||
</body>
|
||||
|
||||
<!-- Adapted from David Basoko's SVG clock demo -->
|
||||
|
||||
33
examples/clock/client/clock.js
Normal file
33
examples/clock/client/clock.js
Normal file
@@ -0,0 +1,33 @@
|
||||
Meteor.setInterval(function () {
|
||||
Session.set('time', new Date);
|
||||
}, 1000);
|
||||
|
||||
UI.body.helpers({
|
||||
|
||||
hours: _.range(0, 12),
|
||||
|
||||
degrees: function () {
|
||||
return 30 * this;
|
||||
},
|
||||
|
||||
handData: function () {
|
||||
var time = Session.get('time') || new Date;
|
||||
return { hourDegrees: time.getHours() * 30,
|
||||
minuteDegrees: time.getMinutes() * 6,
|
||||
secondDegrees: time.getSeconds() * 6 };
|
||||
},
|
||||
|
||||
radial: function (angleDegrees,
|
||||
startFraction,
|
||||
endFraction) {
|
||||
var r = 100;
|
||||
var radians = (angleDegrees-90) / 180 * Math.PI;
|
||||
|
||||
return {
|
||||
x1: r * startFraction * Math.cos(radians),
|
||||
y1: r * startFraction * Math.sin(radians),
|
||||
x2: r * endFraction * Math.cos(radians),
|
||||
y2: r * endFraction * Math.sin(radians)
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -12,7 +12,7 @@ body {
|
||||
|
||||
#outer {
|
||||
width: 600px;
|
||||
margin: 0 auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.player {
|
||||
|
||||
1
examples/other/domrange-grid/.meteor/.gitignore
vendored
Normal file
1
examples/other/domrange-grid/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
10
examples/other/domrange-grid/.meteor/packages
Normal file
10
examples/other/domrange-grid/.meteor/packages
Normal file
@@ -0,0 +1,10 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
standard-app-packages
|
||||
autopublish
|
||||
insecure
|
||||
preserve-inputs
|
||||
ui
|
||||
1
examples/other/domrange-grid/.meteor/release
Normal file
1
examples/other/domrange-grid/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
none
|
||||
18
examples/other/domrange-grid/domrange-grid.css
Normal file
18
examples/other/domrange-grid/domrange-grid.css
Normal file
@@ -0,0 +1,18 @@
|
||||
/* CSS declarations go here */
|
||||
|
||||
|
||||
* { margin: 0; padding: 0 }
|
||||
|
||||
#grid td {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.color0 { background: #eee; }
|
||||
.color1 { background: #d8f; }
|
||||
.color2 { background: #8c3; }
|
||||
.color3 { background: #39d; }
|
||||
.color4 { background: #d96; }
|
||||
.color5 { background: #a95; }
|
||||
7
examples/other/domrange-grid/domrange-grid.html
Normal file
7
examples/other/domrange-grid/domrange-grid.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<head>
|
||||
<title>domrange-grid</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
</body>
|
||||
105
examples/other/domrange-grid/domrange-grid.js
Normal file
105
examples/other/domrange-grid/domrange-grid.js
Normal file
@@ -0,0 +1,105 @@
|
||||
if (Meteor.isClient) {
|
||||
Meteor.startup(function () {
|
||||
var N = 10;
|
||||
var numColors = 6;
|
||||
var colors = [];
|
||||
for(var z = 0; z < numColors; z++)
|
||||
colors[z] = z;
|
||||
|
||||
var guid = 1;
|
||||
|
||||
var table = $('<table id="grid"></table>');
|
||||
$(table).appendTo("body");
|
||||
var rows = [];
|
||||
var tableContent = new UI.DomRange;
|
||||
var makeCell = function (row) {
|
||||
var cells = row.cells;
|
||||
var tr = row.dom.elements()[0];
|
||||
var cell = {color: Random.choice(colors),
|
||||
guid: String(guid++)};
|
||||
cell.dom = new UI.DomRange(cell);
|
||||
cells.push(cell);
|
||||
cell.dom.add(cell.guid, $('<td class="color' +
|
||||
cell.color + '">' +
|
||||
cell.color + '</td>'));
|
||||
row.content.add(cell.guid, cell);
|
||||
};
|
||||
var makeRow = function () {
|
||||
var row = {cells: [], guid: String(guid++),
|
||||
content: new UI.DomRange};
|
||||
row.dom = new UI.DomRange(row);
|
||||
rows.push(row);
|
||||
tableContent.add(row.guid, row);
|
||||
var tr = $("<tr></tr>")[0];
|
||||
row.dom.add(tr);
|
||||
UI.DomRange.insert(row.content, tr);
|
||||
var cells = row.cells;
|
||||
for(var c = 0; c < N; c++)
|
||||
makeCell(row);
|
||||
};
|
||||
for (var r = 0; r < N; r++)
|
||||
makeRow();
|
||||
|
||||
UI.DomRange.insert(tableContent, table[0]);
|
||||
|
||||
$(document).on('keydown', function (evt) {
|
||||
var deltaN = 0;
|
||||
var deltaC = 0;
|
||||
if (evt.which === 38) {
|
||||
deltaN = 1; // up
|
||||
} else if (evt.which === 40) {
|
||||
deltaN = -1; // down
|
||||
} else if (evt.which === 37) {
|
||||
deltaC = -1; // left
|
||||
} else if (evt.which === 39) {
|
||||
deltaC = 1; // right
|
||||
} else if (evt.which === 32) {
|
||||
// spacebar
|
||||
var row0 = rows.shift();
|
||||
rows.push(row0);
|
||||
tableContent.moveBefore(row0.guid, null);
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var row = rows[i];
|
||||
var cell0 = row.cells.shift();
|
||||
row.cells.push(cell0);
|
||||
row.content.moveBefore(cell0.guid, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (deltaN === 1) {
|
||||
N += 1;
|
||||
for (var i = 0; i < N - 1; i++)
|
||||
// lengthen old rows
|
||||
makeCell(rows[i]);
|
||||
makeRow();
|
||||
} else if (deltaN === -1) {
|
||||
if (N === 0)
|
||||
return;
|
||||
N -= 1;
|
||||
tableContent.remove(rows[N].guid);
|
||||
rows.length = N;
|
||||
for (var i = 0; i < N; i++) {
|
||||
var row = rows[i];
|
||||
row.content.remove(row.cells[N].guid);
|
||||
rows[i].cells.length = N;
|
||||
}
|
||||
}
|
||||
|
||||
if (deltaC) {
|
||||
for (var r = 0; r < N; r++) {
|
||||
var row = rows[r];
|
||||
for (var c = 0; c < N; c++) {
|
||||
var cell = row.cells[c];
|
||||
var td = cell.dom.elements()[0];
|
||||
var color =
|
||||
(cell.color =
|
||||
(cell.color + deltaC + numColors)
|
||||
% numColors);
|
||||
td.innerHTML = color;
|
||||
td.className = 'color' + color;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -176,7 +176,7 @@ X={{x}}<br>
|
||||
<br>
|
||||
{{count}} circles<br>
|
||||
<input type="button" value="Add" class="add">
|
||||
<input type="button" value="Remove" class="remove" {{{disabled}}}>
|
||||
<input type="button" value="Remove" class="remove" {{disabled}}>
|
||||
<input type="button" value="Scram" class="scram">
|
||||
<input type="button" value="Clear" class="clear">
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ if (typeof Session.get("spinForward") !== 'boolean') {
|
||||
Template.preserveDemo.preserve([ '.spinner', '.spinforward' ]);
|
||||
|
||||
Template.preserveDemo.spinForwardChecked = function () {
|
||||
return Session.get('spinForward') ? 'checked="checked"' : '';
|
||||
return Session.get('spinForward') ? 'checked' : '';
|
||||
};
|
||||
|
||||
Template.preserveDemo.spinAnim = function () {
|
||||
@@ -69,7 +69,7 @@ Template.preserveDemo.events({
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Template.constantDemo.checked = function (which) {
|
||||
return Session.get('mapchecked' + which) ? 'checked="checked"' : '';
|
||||
return Session.get('mapchecked' + which) ? 'checked' : '';
|
||||
};
|
||||
|
||||
Template.constantDemo.show = function (which) {
|
||||
@@ -193,7 +193,7 @@ Template.circles.count = function () {
|
||||
|
||||
Template.circles.disabled = function () {
|
||||
return Session.get("selectedCircle:" + this.group) ?
|
||||
'' : 'disabled="disabled"';
|
||||
'' : 'disabled';
|
||||
};
|
||||
|
||||
Template.circles.created = function () {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<div class="span5">
|
||||
<div style="float: right">
|
||||
{{loginButtons align="right"}}
|
||||
{{> loginButtons align="right"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,13 +54,11 @@
|
||||
|
||||
<template name="map">
|
||||
<div class="map">
|
||||
{{#constant}}
|
||||
<svg width="500" height="500">
|
||||
<circle class="callout" cx=-100 cy=-100></circle>
|
||||
<g class="circles"></g>
|
||||
<g class="labels"></g>
|
||||
</svg>
|
||||
{{/constant}}
|
||||
<svg width="500" height="500">
|
||||
<circle class="callout" cx=-100 cy=-100></circle>
|
||||
<g class="circles"></g>
|
||||
<g class="labels"></g>
|
||||
</svg>
|
||||
<div>
|
||||
<small class="attribution muted">©
|
||||
<a href="http://www.openstreetmap.org/?lat=37.78212&lon=-122.40146&zoom=15&layers=M"
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
{{else}}
|
||||
<div class="destroy"></div>
|
||||
<div class="display">
|
||||
<input class="check" name="markdone" type="checkbox" {{{done_checkbox}}} />
|
||||
<input class="check" name="markdone" type="checkbox" checked={{done}} />
|
||||
<div class="todo-text">{{text}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -188,10 +188,6 @@ Template.todo_item.done_class = function () {
|
||||
return this.done ? 'done' : '';
|
||||
};
|
||||
|
||||
Template.todo_item.done_checkbox = function () {
|
||||
return this.done ? 'checked="checked"' : '';
|
||||
};
|
||||
|
||||
Template.todo_item.editing = function () {
|
||||
return Session.equals('editing_itemname', this._id);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
</body>
|
||||
|
||||
<template name="radio">
|
||||
<span class="radio"><input id="{{key}}:{{value}}" {{{maybeChecked}}} type="radio" name="{{key}}" value="{{value}}" />{{! no whitespace}}<label for="{{key}}:{{value}}">{{label}}</label></span>
|
||||
<span class="radio">
|
||||
<input id="{{key}}:{{value}}" {{maybeChecked}} type="radio" name="{{key}}" value="{{value}}" />
|
||||
<label for="{{key}}:{{value}}">{{label}}</label>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template name="button">
|
||||
@@ -18,92 +21,92 @@
|
||||
<div id="controlpane">
|
||||
<div class="group">
|
||||
<h3>Dropdown align edge:</h3>
|
||||
{{radio "alignRight" "false" "Left"}}
|
||||
{{radio "alignRight" "true" "Right"}}
|
||||
{{> radio key="alignRight" value="false" label="Left"}}
|
||||
{{> radio key="alignRight" value="true" label="Right"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Positioning:</h3>
|
||||
{{radio "positioning" "relative" "Relative"}}
|
||||
{{radio "positioning" "absolute" "Absolute"}}
|
||||
{{radio "positioning" "floatRight" "Float:right"}}
|
||||
{{radio "positioning" "inline" "Inline"}}
|
||||
{{> radio key="positioning" value="relative" label="Relative"}}
|
||||
{{> radio key="positioning" value="absolute" label="Absolute"}}
|
||||
{{> radio key="positioning" value="floatRight" label="Float:right"}}
|
||||
{{> radio key="positioning" value="inline" label="Inline"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>How many third-party services?</h3>
|
||||
{{radio "numServices" "0" "0"}}
|
||||
{{radio "numServices" "1" "1"}}
|
||||
{{radio "numServices" "2" "2"}}
|
||||
{{radio "numServices" "3" "3"}}
|
||||
{{> radio key="numServices" value="0" label="0"}}
|
||||
{{> radio key="numServices" value="1" label="1"}}
|
||||
{{> radio key="numServices" value="2" label="2"}}
|
||||
{{> radio key="numServices" value="3" label="3"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Has password accounts?</h3>
|
||||
{{radio "hasPasswords" "false" "No"}}
|
||||
{{radio "hasPasswords" "true" "Yes"}}
|
||||
{{> radio key="hasPasswords" value="false" label="No"}}
|
||||
{{> radio key="hasPasswords" value="true" label="Yes"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Password sign-up fields:</h3>
|
||||
{{radio "signupFields" "EMAIL_ONLY" "Email"}}
|
||||
{{radio "signupFields" "USERNAME_ONLY" "Username"}}
|
||||
{{radio "signupFields" "USERNAME_AND_EMAIL" "Username & Email"}}
|
||||
{{radio "signupFields" "USERNAME_AND_OPTIONAL_EMAIL" "Username & Optional Email"}}
|
||||
{{> radio key="signupFields" value="EMAIL_ONLY" label="Email"}}
|
||||
{{> radio key="signupFields" value="USERNAME_ONLY" label="Username"}}
|
||||
{{> radio key="signupFields" value="USERNAME_AND_EMAIL" label="Username & Email"}}
|
||||
{{> radio key="signupFields" value="USERNAME_AND_OPTIONAL_EMAIL" label="Username & Optional Email"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Fake-Configure:</h3>
|
||||
{{button "fakeConfig" "facebook" "Facebook"}}
|
||||
{{button "fakeConfig" "github" "GitHub"}}
|
||||
{{button "fakeConfig" "google" "Google"}}
|
||||
{{> button key="fakeConfig" value="facebook" label="Facebook"}}
|
||||
{{> button key="fakeConfig" value="github" label="GitHub"}}
|
||||
{{> button key="fakeConfig" value="google" label="Google"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Show Configure Dialog:</h3>
|
||||
{{button "showConfig" "facebook" "Facebook"}}
|
||||
{{button "showConfig" "github" "GitHub"}}
|
||||
{{button "showConfig" "google" "Google"}}
|
||||
{{> button key="showConfig" value="facebook" label="Facebook"}}
|
||||
{{> button key="showConfig" value="github" label="GitHub"}}
|
||||
{{> button key="showConfig" value="google" label="Google"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Unconfigure:</h3>
|
||||
{{button "unconfig" "facebook" "Facebook"}}
|
||||
{{button "unconfig" "github" "GitHub"}}
|
||||
{{button "unconfig" "google" "Google"}}
|
||||
{{> button key="unconfig" value="facebook" label="Facebook"}}
|
||||
{{> button key="unconfig" value="github" label="GitHub"}}
|
||||
{{> button key="unconfig" value="google" label="Google"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Messages:</h3>
|
||||
{{button "messages" "error" "Error"}}
|
||||
{{button "messages" "info" "Info"}}
|
||||
{{button "messages" "clear" "Clear"}}
|
||||
{{> button key="messages" value="error" label="Error"}}
|
||||
{{> button key="messages" value="info" label="Info"}}
|
||||
{{> button key="messages" value="clear" label="Clear"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Signing in/out</h3>
|
||||
{{button "sign" "in" "Fake sign-in"}}
|
||||
{{button "sign" "out" "Sign out"}}
|
||||
{{> button key="sign" value="in" label="Fake sign-in"}}
|
||||
{{> button key="sign" value="out" label="Sign out"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Logged-out Views</h3>
|
||||
{{button "lov" "signIn" "Sign In"}}
|
||||
{{button "lov" "createAccount" "Create Account"}}
|
||||
{{button "lov" "forgotPassword" "Forgot Password"}}
|
||||
{{> button key="lov" value="signIn" label="Sign In"}}
|
||||
{{> button key="lov" value="createAccount" label="Create Account"}}
|
||||
{{> button key="lov" value="forgotPassword" label="Forgot Password"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Logged-in Views</h3>
|
||||
{{button "liv" "accountButtons" "Account Buttons"}}
|
||||
{{button "liv" "changePassword" "Change Password"}}
|
||||
{{button "liv" "messageOnly" "Message Only"}}
|
||||
{{> button key="liv" value="accountButtons" label="Account Buttons"}}
|
||||
{{> button key="liv" value="changePassword" label="Change Password"}}
|
||||
{{> button key="liv" value="messageOnly" label="Message Only"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Other Modals</h3>
|
||||
{{button "modals" "resetPassword" "Reset Password"}}
|
||||
{{button "modals" "enrollAccount" "Enroll Account"}}
|
||||
{{button "modals" "justVerifiedEmail" "Verified Email"}}
|
||||
{{> button key="modals" value="resetPassword" label="Reset Password"}}
|
||||
{{> button key="modals" value="enrollAccount" label="Enroll Account"}}
|
||||
{{> button key="modals" value="justVerifiedEmail" label="Verified Email"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Logging-in Spinner</h3>
|
||||
{{radio "fakeLoggingIn" "false" "Off"}}
|
||||
{{radio "fakeLoggingIn" "true" "Pretend loggingIn=true"}}
|
||||
{{> radio key="fakeLoggingIn" value="false" label="Off"}}
|
||||
{{> radio key="fakeLoggingIn" value="true" label="Pretend loggingIn=true"}}
|
||||
</div>
|
||||
<div class="group">
|
||||
<h3>Background Color</h3>
|
||||
{{radio "bgcolor" "white" "White"}}
|
||||
{{radio "bgcolor" "black" "Black"}}
|
||||
{{radio "bgcolor" "red" "Red"}}
|
||||
{{> radio key="bgcolor" value="white" label="White"}}
|
||||
{{> radio key="bgcolor" value="black" label="Black"}}
|
||||
{{> radio key="bgcolor" value="red" label="Red"}}
|
||||
</div>
|
||||
</div>
|
||||
{{#with settings}}
|
||||
@@ -112,7 +115,7 @@
|
||||
{{#if match "positioning:inline"}}
|
||||
Here is a place to sign in, yay!
|
||||
{{/if}}
|
||||
{{loginButtons align=dropdownAlign}}
|
||||
{{> loginButtons align=dropdownAlign}}
|
||||
{{#if match "positioning:inline"}}
|
||||
Isn't that great?
|
||||
{{/if}}
|
||||
|
||||
@@ -87,20 +87,10 @@ if (Meteor.isClient) {
|
||||
Template.radio.maybeChecked = function () {
|
||||
var curValue = Session.get('settings')[this.key];
|
||||
if (castValue(this.value) === curValue)
|
||||
return 'checked="checked"';
|
||||
return 'checked';
|
||||
return '';
|
||||
};
|
||||
|
||||
Template.page.radio = function (key, value, label) {
|
||||
return new Handlebars.SafeString(
|
||||
Template.radio({key: key, value: value, label: label}));
|
||||
};
|
||||
|
||||
Template.page.button = function (key, value, label) {
|
||||
return new Handlebars.SafeString(
|
||||
Template.button({key: key, value: value, label: label}));
|
||||
};
|
||||
|
||||
Template.page.match = function (kv) {
|
||||
kv = keyValueFromId(kv);
|
||||
if (! kv)
|
||||
@@ -218,4 +208,4 @@ if (Meteor.isClient) {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
1
examples/unfinished/atoms/.meteor/.gitignore
vendored
Normal file
1
examples/unfinished/atoms/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
9
examples/unfinished/atoms/.meteor/packages
Normal file
9
examples/unfinished/atoms/.meteor/packages
Normal file
@@ -0,0 +1,9 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
standard-app-packages
|
||||
autopublish
|
||||
insecure
|
||||
underscore
|
||||
1
examples/unfinished/atoms/.meteor/release
Normal file
1
examples/unfinished/atoms/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
none
|
||||
12
examples/unfinished/atoms/atoms.css
Normal file
12
examples/unfinished/atoms/atoms.css
Normal file
@@ -0,0 +1,12 @@
|
||||
g[class=atom] circle {
|
||||
stroke: black;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
g[class=atom] text {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 24px;
|
||||
fill: black;
|
||||
font-weight: bold;
|
||||
text-anchor: middle;
|
||||
}
|
||||
28
examples/unfinished/atoms/atoms.html
Normal file
28
examples/unfinished/atoms/atoms.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<head>
|
||||
<title>Atoms</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="atoms">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 500 500">
|
||||
{{#atom x=100 y=100 color="#ffff00"}}O{{/atom}}
|
||||
{{> hydrogen x=150 y=100}}
|
||||
{{#giantatom x=250 y=100 color="#ff9999"}}My{{/giantatom}}
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<template name="atom">
|
||||
<g class="atom">
|
||||
<circle style="fill: {{#if color}}{{color}}{{else}}white{{/if}}" cx={{x}} cy={{y}} r={{#if r}}{{r}}{{else}}20{{/if}} />
|
||||
<text x={{x}} y={{textY}}>{{> UI.contentBlock}}</text>
|
||||
</g>
|
||||
</template>
|
||||
|
||||
<template name="hydrogen">
|
||||
{{#atom x=x y=y r=r color="#00ffff"}}H{{/atom}}
|
||||
</template>
|
||||
|
||||
<template name="giantatom">
|
||||
{{#atom x=x y=y r=50 color=color}}{{> UI.contentBlock}}{{/atom}}
|
||||
</template>
|
||||
5
examples/unfinished/atoms/atoms.js
Normal file
5
examples/unfinished/atoms/atoms.js
Normal file
@@ -0,0 +1,5 @@
|
||||
if (Meteor.isClient) {
|
||||
Template.atom.textY = function () {
|
||||
return this.y + 8;
|
||||
};
|
||||
}
|
||||
1
examples/unfinished/movers/.meteor/.gitignore
vendored
Normal file
1
examples/unfinished/movers/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
10
examples/unfinished/movers/.meteor/packages
Normal file
10
examples/unfinished/movers/.meteor/packages
Normal file
@@ -0,0 +1,10 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
standard-app-packages
|
||||
autopublish
|
||||
insecure
|
||||
preserve-inputs
|
||||
less
|
||||
1
examples/unfinished/movers/.meteor/release
Normal file
1
examples/unfinished/movers/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
none
|
||||
2252
examples/unfinished/movers/client/jquery-ui-sortable.js
vendored
Executable file
2252
examples/unfinished/movers/client/jquery-ui-sortable.js
vendored
Executable file
File diff suppressed because it is too large
Load Diff
16
examples/unfinished/movers/movers.html
Normal file
16
examples/unfinished/movers/movers.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<head>
|
||||
<title>movers</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{> main}}
|
||||
</body>
|
||||
|
||||
<template name="main">
|
||||
<div class="container">
|
||||
<div class="item red">Red</div>
|
||||
<div class="item green">Green</div>
|
||||
<div class="item blue">Blue</div>
|
||||
<div class="item yellow">Yellow</div>
|
||||
</div>
|
||||
</template>
|
||||
92
examples/unfinished/movers/movers.js
Normal file
92
examples/unfinished/movers/movers.js
Normal file
@@ -0,0 +1,92 @@
|
||||
if (Meteor.isClient) {
|
||||
var moveCount = 0;
|
||||
var MOVE_INTERVAL = 3000;
|
||||
var MOVE_DURATION = 2000;
|
||||
|
||||
doMove = function () {
|
||||
moveCount++;
|
||||
if (moveCount % 2 === 1) {
|
||||
animateToBefore($('.green'), $('.yellow'));
|
||||
animateToBefore($('.red'), null);
|
||||
animateToBefore($('.blue'), null);
|
||||
} else {
|
||||
animateToBefore($('.red'), null);
|
||||
animateToBefore($('.green'), null);
|
||||
animateToBefore($('.blue'), null);
|
||||
animateToBefore($('.yellow'), null);
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.startup(function () {
|
||||
doMove();
|
||||
window.setInterval(doMove, MOVE_INTERVAL);
|
||||
});
|
||||
|
||||
|
||||
animateToBefore = function ($n, $newNext) {
|
||||
// we don't use jQuery's `.css()` for these because we want the
|
||||
// element's own style, not the computed style
|
||||
var oldTop = $n[0].style.top;
|
||||
var oldPosition = $n[0].style.position;
|
||||
var oldZIndex = $n[0].style.zIndex;
|
||||
var oldMarginBottom = $n[0].style.marginBottom;
|
||||
|
||||
var outerHeight = $n.outerHeight(); // not margin
|
||||
var marginBottom = parseInt($n.css('margin-bottom'));
|
||||
|
||||
// TODO: test interesting elements like table rows, etc.
|
||||
var placeholder = $(document.createElement($n[0].nodeName));
|
||||
var placeholderHeight = outerHeight + marginBottom;
|
||||
placeholder.css('height', placeholderHeight);
|
||||
// insert placeholder
|
||||
$n.before(placeholder);
|
||||
|
||||
// move node
|
||||
if ($newNext)
|
||||
$newNext.before($n);
|
||||
else
|
||||
$n.parent().append($n);
|
||||
|
||||
// XXX would tracking "left" as well as "top" magically get us
|
||||
// horizontal re-ordering?
|
||||
$n.css({marginBottom: -outerHeight,
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
top: 0});
|
||||
var vOffset = placeholder.offset().top - $n.offset().top;
|
||||
$n.css('top', vOffset);
|
||||
|
||||
$({t:0}).animate({t:1}, {
|
||||
duration: MOVE_DURATION,
|
||||
step: function (t, fx) {
|
||||
var curPlaceholderHeight = Math.round(placeholderHeight * (1-t));
|
||||
var curMarginBottom = marginBottom - curPlaceholderHeight;
|
||||
var curTop = (-curPlaceholderHeight +
|
||||
Math.round((1-t) * (vOffset + placeholderHeight)));
|
||||
$n.css({marginBottom: curMarginBottom,
|
||||
top: curTop});
|
||||
placeholder.css('height', curPlaceholderHeight);
|
||||
},
|
||||
progress: function (a, t) {
|
||||
// if (t >= 0.5) {
|
||||
// console.log(a);
|
||||
// a.stop();
|
||||
// }
|
||||
},
|
||||
complete: function () {
|
||||
placeholder.remove();
|
||||
$n[0].style.top = oldTop;
|
||||
$n[0].style.position = oldPosition;
|
||||
$n[0].style.zIndex = oldZIndex;
|
||||
$n[0].style.marginBottom = oldMarginBottom;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Meteor.startup(function () {
|
||||
// code to run on server at startup
|
||||
});
|
||||
}
|
||||
12
examples/unfinished/movers/movers.less
Normal file
12
examples/unfinished/movers/movers.less
Normal file
@@ -0,0 +1,12 @@
|
||||
.item {
|
||||
border: 1px solid black;
|
||||
padding: 10px;
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
}
|
||||
.red { background: #fcc; }
|
||||
.blue { background: #ccf; }
|
||||
.green { background: #cfc; }
|
||||
.yellow { background: #ffc; }
|
||||
1
examples/unfinished/reorderable-list/.meteor/.gitignore
vendored
Normal file
1
examples/unfinished/reorderable-list/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
8
examples/unfinished/reorderable-list/.meteor/packages
Normal file
8
examples/unfinished/reorderable-list/.meteor/packages
Normal file
@@ -0,0 +1,8 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
standard-app-packages
|
||||
autopublish
|
||||
insecure
|
||||
1
examples/unfinished/reorderable-list/.meteor/release
Normal file
1
examples/unfinished/reorderable-list/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
none
|
||||
2252
examples/unfinished/reorderable-list/client/jquery-ui-sortable.js
vendored
Executable file
2252
examples/unfinished/reorderable-list/client/jquery-ui-sortable.js
vendored
Executable file
File diff suppressed because it is too large
Load Diff
10
examples/unfinished/reorderable-list/client/shark.css
Normal file
10
examples/unfinished/reorderable-list/client/shark.css
Normal file
@@ -0,0 +1,10 @@
|
||||
#list div {
|
||||
padding: 10px;
|
||||
height: 19px;
|
||||
border: 1px solid #bbb;
|
||||
margin: 8px;
|
||||
font-weight: bold;
|
||||
cursor: move;
|
||||
width: 400px;
|
||||
background: #eee; }
|
||||
|
||||
9
examples/unfinished/reorderable-list/client/shark.html
Normal file
9
examples/unfinished/reorderable-list/client/shark.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<body>
|
||||
<div id="list">
|
||||
{{#each items}}
|
||||
<div class="item">
|
||||
{{text}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</body>
|
||||
30
examples/unfinished/reorderable-list/client/shark.js
Normal file
30
examples/unfinished/reorderable-list/client/shark.js
Normal file
@@ -0,0 +1,30 @@
|
||||
UI.body.items = Items.find({}, { sort: { rank: 1 } });
|
||||
|
||||
SimpleRationalRanks = {
|
||||
beforeFirst: function (firstRank) { return firstRank - 1; },
|
||||
between: function (beforeRank, afterRank) { return (beforeRank + afterRank) / 2; },
|
||||
afterLast: function (lastRank) { return lastRank + 1; }
|
||||
};
|
||||
|
||||
UI.body.rendered = function () {
|
||||
$(this.find('#list')).sortable({ // uses the 'sortable' interaction from jquery ui
|
||||
stop: function (event, ui) { // fired when an item is dropped
|
||||
var el = ui.item.get(0), before = ui.item.prev().get(0), after = ui.item.next().get(0);
|
||||
|
||||
var newRank;
|
||||
if (!before) { // moving to the top of the list
|
||||
newRank = SimpleRationalRanks.beforeFirst(UI.getElementData(after).rank);
|
||||
|
||||
} else if (!after) { // moving to the bottom of the list
|
||||
newRank = SimpleRationalRanks.afterLast(UI.getElementData(before).rank);
|
||||
|
||||
} else {
|
||||
newRank = SimpleRationalRanks.between(
|
||||
UI.getElementData(before).rank,
|
||||
UI.getElementData(after).rank);
|
||||
}
|
||||
|
||||
Items.update(UI.getElementData(el)._id, {$set: {rank: newRank}});
|
||||
}
|
||||
});
|
||||
};
|
||||
9
examples/unfinished/reorderable-list/lib/items.js
Normal file
9
examples/unfinished/reorderable-list/lib/items.js
Normal file
@@ -0,0 +1,9 @@
|
||||
Items = new Meteor.Collection("items");
|
||||
|
||||
if (Meteor.isServer) {
|
||||
if (Items.find().count() === 0) {
|
||||
_.each(
|
||||
["violet", "unicorn", "flask", "jar", "leitmotif", "rearrange", "right", "ethereal"],
|
||||
function (text, index) { Items.insert({text: text, rank: index}); });
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
<div>
|
||||
<button id="startgame" class="startgame" {{{disabled}}}>
|
||||
<button id="startgame" class="startgame" {{disabled}}>
|
||||
{{#if count}} It's on! {{else}} Play solo {{/if}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,9 @@ var set_selected_positions = function (word) {
|
||||
}
|
||||
|
||||
for (var pos = 0; pos < 16; pos++) {
|
||||
if (last_in_a_path.indexOf(pos) !== -1)
|
||||
if (_.indexOf(last_in_a_path, pos) !== -1)
|
||||
Session.set('selected_' + pos, 'last_in_path');
|
||||
else if (in_a_path.indexOf(pos) !== -1)
|
||||
else if (_.indexOf(in_a_path, pos) !== -1)
|
||||
Session.set('selected_' + pos, 'in_path');
|
||||
else
|
||||
Session.set('selected_' + pos, false);
|
||||
@@ -68,13 +68,14 @@ Template.lobby.disabled = function () {
|
||||
var me = player();
|
||||
if (me && me.name)
|
||||
return '';
|
||||
return 'disabled="disabled"';
|
||||
return 'disabled';
|
||||
};
|
||||
|
||||
var trim = function (string) { return string.replace(/^\s+|\s+$/g, ''); };
|
||||
|
||||
Template.lobby.events({
|
||||
'keyup input#myname': function (evt) {
|
||||
var name = $('#lobby input#myname').val().trim();
|
||||
var name = trim($('#lobby input#myname').val());
|
||||
Players.update(Session.get('player_id'), {$set: {name: name}});
|
||||
},
|
||||
'click button.startgame': function () {
|
||||
@@ -115,7 +116,9 @@ Template.board.clock = function () {
|
||||
Template.board.events({
|
||||
'click .square': function (evt) {
|
||||
var textbox = $('#scratchpad input');
|
||||
textbox.val(textbox.val() + evt.target.innerHTML);
|
||||
// Note: Getting the letter out of the DOM is kind of a hack
|
||||
var letter = evt.target.textContent || evt.target.innerText;
|
||||
textbox.val(textbox.val() + letter);
|
||||
textbox.focus();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ paths_for_word = function (board, word) {
|
||||
|
||||
for (var i = 0; i < positions_to_try.length; i++) {
|
||||
var pos = positions_to_try[i];
|
||||
if (board[pos] === word[0] && path.indexOf(pos) === -1)
|
||||
if (board[pos] === word[0] && _.indexOf(path, pos) === -1)
|
||||
check_path(word.slice(1), // cdr of word
|
||||
path.concat([pos]), // append matching loc to path
|
||||
ADJACENCIES[pos]); // only look at surrounding tiles
|
||||
|
||||
@@ -269,13 +269,13 @@ Accounts.loginServicesConfigured = function () {
|
||||
/// HANDLEBARS HELPERS
|
||||
///
|
||||
|
||||
// If we're using Handlebars, register the {{currentUser}} and
|
||||
// {{loggingIn}} global helpers.
|
||||
if (Package.handlebars) {
|
||||
Package.handlebars.Handlebars.registerHelper('currentUser', function () {
|
||||
// If our app has a UI, register the {{currentUser}} and {{loggingIn}}
|
||||
// global helpers.
|
||||
if (Package.ui) {
|
||||
Package.ui.UI.registerHelper('currentUser', function () {
|
||||
return Meteor.user();
|
||||
});
|
||||
Package.handlebars.Handlebars.registerHelper('loggingIn', function () {
|
||||
Package.ui.UI.registerHelper('loggingIn', function () {
|
||||
return Meteor.loggingIn();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ Package.on_use(function (api) {
|
||||
// we'd probably want to abstract this away
|
||||
api.use('mongo-livedata', ['client', 'server']);
|
||||
|
||||
// If handlebars happens to be loaded, we'll define some helpers like
|
||||
// If the 'ui' package is loaded, we'll define some helpers like
|
||||
// {{currentUser}}. If not, no biggie.
|
||||
api.use('handlebars', 'client', {weak: true});
|
||||
api.use('ui', 'client', {weak: true});
|
||||
|
||||
// Allow us to detect 'autopublish', and publish some Meteor.users fields if
|
||||
// it's loaded.
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
<!--
|
||||
NOTE: You shouldn't use these templates directly. Instead, use the global
|
||||
{{loginButtons}} template. For positioning on the right side of your app,
|
||||
try {{loginButtons align="right"}}
|
||||
-->
|
||||
|
||||
<template name="_loginButtons">
|
||||
<template name="loginButtons">
|
||||
<div id="login-buttons" class="login-buttons-dropdown-align-{{align}}">
|
||||
{{#if currentUser}}
|
||||
{{#if loggingIn}}
|
||||
@@ -86,6 +80,6 @@
|
||||
</div>
|
||||
{{else}}
|
||||
{{! just add some padding }}
|
||||
<div class="login-buttons-padding" />
|
||||
<div class="login-buttons-padding"></div>
|
||||
{{/unless}}
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
// for convenience
|
||||
var loginButtonsSession = Accounts._loginButtonsSession;
|
||||
|
||||
Handlebars.registerHelper(
|
||||
"loginButtons",
|
||||
function (options) {
|
||||
if (options.hash.align === "right")
|
||||
return new Handlebars.SafeString(Template._loginButtons({align: "right"}));
|
||||
else
|
||||
return new Handlebars.SafeString(Template._loginButtons({align: "left"}));
|
||||
});
|
||||
|
||||
// shared between dropdown and single mode
|
||||
Template._loginButtons.events({
|
||||
Template.loginButtons.events({
|
||||
'click #login-buttons-logout': function() {
|
||||
Meteor.logout(function () {
|
||||
loginButtonsSession.closeDropdown();
|
||||
@@ -19,8 +10,8 @@ Template._loginButtons.events({
|
||||
}
|
||||
});
|
||||
|
||||
Template._loginButtons.preserve({
|
||||
'input[id]': Spark._labelFromIdOrName
|
||||
UI.registerHelper('loginButtons', function () {
|
||||
throw new Error("Use {{> loginButtons}} instead of {{loginButtons}}");
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<template name="_configureLoginServiceDialog">
|
||||
{{#if visible}}
|
||||
<div id="configure-login-service-dialog" class="accounts-dialog accounts-centered-dialog">
|
||||
{{{configurationSteps}}}
|
||||
{{> configurationSteps}}
|
||||
|
||||
<p>
|
||||
Now, copy over some details.
|
||||
@@ -97,12 +97,10 @@
|
||||
</div>
|
||||
<a class="accounts-close configure-login-service-dismiss-button">×</a>
|
||||
|
||||
{{#isolate}}
|
||||
<div class="login-button login-button-configure {{#if saveDisabled}}login-button-disabled{{/if}}"
|
||||
<div class="login-button login-button-configure {{#if saveDisabled}}login-button-disabled{{/if}}"
|
||||
id="configure-login-service-dialog-save-configuration">
|
||||
Save Configuration
|
||||
</div>
|
||||
{{/isolate}}
|
||||
Save Configuration
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -231,7 +231,7 @@ Template._configureLoginServiceDialog.visible = function () {
|
||||
|
||||
Template._configureLoginServiceDialog.configurationSteps = function () {
|
||||
// renders the appropriate template
|
||||
return configureLoginServiceDialogTemplateForService()();
|
||||
return configureLoginServiceDialogTemplateForService();
|
||||
};
|
||||
|
||||
Template._configureLoginServiceDialog.saveDisabled = function () {
|
||||
|
||||
@@ -3,7 +3,7 @@ var loginButtonsSession = Accounts._loginButtonsSession;
|
||||
|
||||
// events shared between loginButtonsLoggedOutDropdown and
|
||||
// loginButtonsLoggedInDropdown
|
||||
Template._loginButtons.events({
|
||||
Template.loginButtons.events({
|
||||
'click #login-name-link, click #login-sign-in-link': function () {
|
||||
loginButtonsSession.set('dropdownVisible', true);
|
||||
Deps.flush();
|
||||
@@ -94,7 +94,9 @@ Template._loginButtonsLoggedOutDropdown.events({
|
||||
document.getElementById('login-username').value = usernameOrEmail;
|
||||
else
|
||||
document.getElementById('login-email').value = usernameOrEmail;
|
||||
// "login-password" is preserved, since password fields aren't updated by Spark.
|
||||
|
||||
if (password !== null)
|
||||
document.getElementById('login-password').value = password;
|
||||
|
||||
// Force redrawing the `login-dropdown-list` element because of
|
||||
// a bizarre Chrome bug in which part of the DIV is not redrawn
|
||||
@@ -134,6 +136,8 @@ Template._loginButtonsLoggedOutDropdown.events({
|
||||
var username = trimmedElementValueById('login-username');
|
||||
var email = trimmedElementValueById('login-email')
|
||||
|| trimmedElementValueById('forgot-password-email'); // Ughh. Standardize on names?
|
||||
// notably not trimmed. a password could (?) start or end with a space
|
||||
var password = elementValueById('login-password');
|
||||
|
||||
loginButtonsSession.set('inSignupFlow', false);
|
||||
loginButtonsSession.set('inForgotPasswordFlow', false);
|
||||
@@ -144,9 +148,12 @@ Template._loginButtonsLoggedOutDropdown.events({
|
||||
document.getElementById('login-username').value = username;
|
||||
if (document.getElementById('login-email'))
|
||||
document.getElementById('login-email').value = email;
|
||||
// "login-password" is preserved, since password fields aren't updated by Spark.
|
||||
|
||||
if (document.getElementById('login-username-or-email'))
|
||||
document.getElementById('login-username-or-email').value = email || username;
|
||||
|
||||
if (password !== null)
|
||||
document.getElementById('login-password').value = password;
|
||||
},
|
||||
'keypress #login-username, keypress #login-email, keypress #login-username-or-email, keypress #login-password, keypress #login-password-again': function (event) {
|
||||
if (event.keyCode === 13)
|
||||
|
||||
@@ -4,8 +4,7 @@ Package.describe({
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['deps', 'service-configuration', 'accounts-base',
|
||||
'underscore', 'templating',
|
||||
'handlebars', 'spark', 'session'], 'client');
|
||||
'underscore', 'templating', 'session'], 'client');
|
||||
// Export Accounts (etc) to packages using this one.
|
||||
api.imply('accounts-base', ['client', 'server']);
|
||||
|
||||
|
||||
1
packages/animation/.gitignore
vendored
Normal file
1
packages/animation/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
271
packages/animation/animated_each.js
Normal file
271
packages/animation/animated_each.js
Normal file
@@ -0,0 +1,271 @@
|
||||
// To use this package, there are a few restrictions on your markup
|
||||
// and css:
|
||||
//
|
||||
// - Elements being animated may not have margin-top set. This is
|
||||
// because of margin collapsing.
|
||||
// - Elements are expected to have position "relative" or "static"
|
||||
// (the default).
|
||||
// - Elements must have top "auto" or "0", because "top" is used to
|
||||
// animate moves.
|
||||
|
||||
var ANIMATION_DURATION = 500;
|
||||
|
||||
// animate margin-bottom more quickly than height
|
||||
var MARGIN_ACCEL = 4;
|
||||
|
||||
// Assumes `$n` represents one element.
|
||||
var scaleTowardsTop = function ($n, fraction) {
|
||||
var t = fraction;
|
||||
|
||||
// Todo: Make this work in IE8.
|
||||
// - Use feature detection (see link) to detect whether we
|
||||
// have "transform" (with any vendor prefix),
|
||||
// otherwise whether we have "filter".
|
||||
// https://github.com/louisremi/jquery.transform.js/blob/master/jquery.transform2d.js
|
||||
// - For IE 8, set 'filter' to
|
||||
// something like
|
||||
// `"progid:DXImageTransform.Microsoft.Matrix(M11=1, M12=0, M21=0, M22=0.5, SizingMethod='auto expand')"`
|
||||
$n.css({transform: 'translateY(' +
|
||||
(-(1-t)/2*100) + '%) scaleY(' + t + ')'});
|
||||
};
|
||||
|
||||
var removeScale = function ($n) {
|
||||
// jQuery handles the vendor prefix for us
|
||||
// (like -webkit-transform)
|
||||
// See: http://caniuse.com/#feat=transforms2d
|
||||
$n.css({transform: ''});
|
||||
};
|
||||
|
||||
var ANIMATION_STATE_EXPANDO = '_meteorUIAnimateState';
|
||||
|
||||
var getAnimationState = function ($n) {
|
||||
var n = $n[0];
|
||||
var state = n[ANIMATION_STATE_EXPANDO];
|
||||
if (! state) {
|
||||
state = (n[ANIMATION_STATE_EXPANDO] = {});
|
||||
// values in "style" attribute for restoring at end
|
||||
state.ownMarginBottom = n.style.marginBottom;
|
||||
state.ownOpacity = n.style.opacity;
|
||||
// computed styles to animate towards on insert
|
||||
state.fullMarginBottom = parseInt($n.css('margin-bottom'), 10);
|
||||
state.fullOpacity = parseFloat($n.css('opacity'));
|
||||
state.fullHeight = $n.outerHeight(); // border box, w/o margin
|
||||
|
||||
state.currentAnimation = null;
|
||||
state.$n = $n;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
var clearAnimationState = function ($n) {
|
||||
var n = $n[0];
|
||||
try {
|
||||
delete n[ANIMATION_STATE_EXPANDO];
|
||||
} catch (e) {
|
||||
// IE 8 can't delete expandos?
|
||||
n[ANIMATION_STATE_EXPANDO] = null;
|
||||
}
|
||||
};
|
||||
|
||||
var showHideStep = function (t, fx) {
|
||||
var state = fx.elem;
|
||||
var fullHeight = state.fullHeight;
|
||||
var fullMarginBottom = state.fullMarginBottom;
|
||||
var $n = state.$n;
|
||||
var fullOpacity = state.fullOpacity;
|
||||
|
||||
var curMarginBottom = -fullHeight + t*fullHeight +
|
||||
Math.round(Math.min(MARGIN_ACCEL*t, 1) * fullMarginBottom);
|
||||
$n.css({marginBottom: curMarginBottom});
|
||||
scaleTowardsTop($n, t);
|
||||
$n.css({opacity: t*fullOpacity});
|
||||
};
|
||||
|
||||
var apply = function (el, events) {
|
||||
var animateInsert = function (n, parent, next) {
|
||||
parent.insertBefore(n, next);
|
||||
|
||||
var $n = $(n);
|
||||
var state = getAnimationState($n);
|
||||
|
||||
if (state.currentAnimation)
|
||||
state.currentAnimation.stop();
|
||||
else
|
||||
state.t = 0;
|
||||
|
||||
$n.css({marginBottom: -state.outerHeight,
|
||||
opacity: 0});
|
||||
|
||||
$(state).animate({t:1}, {
|
||||
duration: ANIMATION_DURATION,
|
||||
step: showHideStep,
|
||||
// If the animation is queued, then even if stopped it
|
||||
// blocks another animation on the same state object.
|
||||
// Note: other possibilities here, like a named queue
|
||||
queue: false,
|
||||
start: function (fx) {
|
||||
state.currentAnimation = fx;
|
||||
},
|
||||
complete: function () {
|
||||
n.style.marginBottom = state.ownMarginBottom;
|
||||
n.style.opacity = state.ownOpacity;
|
||||
removeScale($n);
|
||||
clearAnimationState($n);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var animateRemove = function (n) {
|
||||
var $n = $(n);
|
||||
var state = getAnimationState($n);
|
||||
|
||||
if (state.currentAnimation)
|
||||
state.currentAnimation.stop();
|
||||
else
|
||||
state.t = 1;
|
||||
|
||||
$(state).animate({t:0}, {
|
||||
duration: ANIMATION_DURATION,
|
||||
step: showHideStep,
|
||||
queue: false,
|
||||
start: function (fx) {
|
||||
state.currentAnimation = fx;
|
||||
},
|
||||
complete: function () {
|
||||
n.parentNode.removeChild(n);
|
||||
clearAnimationState($n);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var MOVE_QUEUE = "meteor-ui-move";
|
||||
|
||||
var animateMove = function (n, next) {
|
||||
var $n = $(n);
|
||||
|
||||
var getFlowHeight = function ($elem) {
|
||||
return $elem.outerHeight() +
|
||||
parseInt($elem.css('margin-bottom'), 10);
|
||||
};
|
||||
|
||||
var addToTop = function ($elem, px) {
|
||||
// We use a queue so that `animateMove` can stop only its own
|
||||
// animations. We don't actually enqueue more than one
|
||||
// animation at a time.
|
||||
$elem.stop(MOVE_QUEUE, true); // clear queue
|
||||
var top = parseInt($elem.css('top'), 10) || 0;
|
||||
top += px;
|
||||
$elem.css({top: top, position: 'relative'});
|
||||
// after setting `top`, animate it to 0.
|
||||
$elem.animate({top: 0},
|
||||
{duration: ANIMATION_DURATION,
|
||||
queue: MOVE_QUEUE,
|
||||
progress: function (a) {
|
||||
// in case we were nabbed by a Sortable or
|
||||
// Draggable or something, end our animation
|
||||
// early.
|
||||
if ($elem.css('position') === 'absolute')
|
||||
a.stop(true); // skip to end of anim
|
||||
}
|
||||
}).dequeue(MOVE_QUEUE);
|
||||
// XXX On complete, we'd like to remove 'position: relative' and
|
||||
// 'top: 0', but we have to be careful if there is an "add"
|
||||
// animation happening. Probably need some cross-animation
|
||||
// mechanism for restoring elements to a pristine state
|
||||
// only when all animations have finished.
|
||||
};
|
||||
|
||||
var flowHeight = getFlowHeight($n);
|
||||
|
||||
var nOffset = 0;
|
||||
|
||||
// Determine the vertical displacement that we expect moving
|
||||
// `n` to create in `n` and the elements between `n` and
|
||||
// `next`, and counteract it by adding appropriate positive
|
||||
// or negative amounts to the `top` of each element.
|
||||
|
||||
var mode = 0;
|
||||
for (var m = n.parentNode.firstChild; m; m = m.nextSibling) {
|
||||
var isElement = (m.nodeType === 1);
|
||||
if (mode === 0) {
|
||||
if (m === n) {
|
||||
mode = 1; // move is downwards
|
||||
} else if (m === next) {
|
||||
mode = 2; // move is upwards
|
||||
if (isElement) {
|
||||
var $m = $(m);
|
||||
addToTop($m, -flowHeight);
|
||||
nOffset += getFlowHeight($m);
|
||||
}
|
||||
}
|
||||
} else if (mode === 1) {
|
||||
// move is downwards, have seen n
|
||||
if (m === next)
|
||||
break;
|
||||
if (isElement) {
|
||||
var $m = $(m);
|
||||
addToTop($m, flowHeight);
|
||||
nOffset -= getFlowHeight($m);
|
||||
}
|
||||
} else if (mode === 2) {
|
||||
// move is upwards, have seen next
|
||||
if (m === n)
|
||||
break;
|
||||
if (isElement) {
|
||||
var $m = $(m);
|
||||
addToTop($m, -flowHeight);
|
||||
nOffset += getFlowHeight($m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addToTop($n, nOffset);
|
||||
|
||||
// move node
|
||||
if (next)
|
||||
n.parentNode.insertBefore(n, next);
|
||||
else
|
||||
n.parentNode.appendChild(n);
|
||||
};
|
||||
|
||||
if ($(el)[0].$uihooks)
|
||||
throw new Error("Can't use #AnimatedEach on an element already decorated with ui hooks");
|
||||
$(el)[0].$uihooks = {};
|
||||
|
||||
events = events || ['add', 'remove', 'move'];
|
||||
|
||||
if (_.contains(events, 'add')) {
|
||||
$(el)[0].$uihooks.insertElement = function (n, parent, next) {
|
||||
animateInsert(n, parent, next);
|
||||
};
|
||||
}
|
||||
|
||||
if (_.contains(events, 'remove')) {
|
||||
$(el)[0].$uihooks.removeElement = function (n) {
|
||||
animateRemove(n);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (_.contains(events, 'move')) {
|
||||
$(el)[0].$uihooks.moveElement = function (n, parent, next) {
|
||||
animateMove(n, next);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
AnimatedList = Package.ui.Component.extend({
|
||||
typeName: 'AnimatedList',
|
||||
render: function (buf) {
|
||||
buf.write(this.content);
|
||||
},
|
||||
attached: function () {
|
||||
var self = this;
|
||||
var childEls = _.filter(self.$('*'), function (n) {
|
||||
return n.parentNode === self.firstNode().parentNode;
|
||||
}); // XXX we'd like something like jquery's `.children()`
|
||||
if (childEls.length !== 1)
|
||||
throw new Error("#AnimatedList must have precisely one top-level child element");
|
||||
apply(childEls, self.events && self.events.split(' '));
|
||||
}
|
||||
});
|
||||
9
packages/animation/package.js
Normal file
9
packages/animation/package.js
Normal file
@@ -0,0 +1,9 @@
|
||||
Package.describe({
|
||||
summary: "A set of commonly used animation decorators"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.export(['AnimatedList']);
|
||||
api.use(['jquery', 'ui']);
|
||||
api.add_files(['animated_each.js'], 'client');
|
||||
});
|
||||
@@ -31,6 +31,20 @@ var _throwOrLog = function (from, e) {
|
||||
}
|
||||
};
|
||||
|
||||
// Like `Meteor._noYieldsAllowed(function () { f(comp); })` but shorter,
|
||||
// and doesn't clutter the stack with an extra frame on the client,
|
||||
// where `_noYieldsAllowed` is a no-op. `f` may be a computation
|
||||
// function or an onInvalidate callback.
|
||||
var callWithNoYieldsAllowed = function (f, comp) {
|
||||
if (Meteor.isClient) {
|
||||
f(comp);
|
||||
} else {
|
||||
Meteor._noYieldsAllowed(function () {
|
||||
f(comp);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var nextId = 1;
|
||||
// computations whose callbacks we should call at flush time
|
||||
var pendingComputations = [];
|
||||
@@ -111,18 +125,13 @@ _.extend(Deps.Computation.prototype, {
|
||||
if (typeof f !== 'function')
|
||||
throw new Error("onInvalidate requires a function");
|
||||
|
||||
var g = function () {
|
||||
if (self.invalidated) {
|
||||
Deps.nonreactive(function () {
|
||||
return Meteor._noYieldsAllowed(function () {
|
||||
f(self);
|
||||
});
|
||||
callWithNoYieldsAllowed(f, self);
|
||||
});
|
||||
};
|
||||
|
||||
if (self.invalidated)
|
||||
g();
|
||||
else
|
||||
self._onInvalidateCallbacks.push(g);
|
||||
} else {
|
||||
self._onInvalidateCallbacks.push(f);
|
||||
}
|
||||
},
|
||||
|
||||
// http://docs.meteor.com/#computation_invalidate
|
||||
@@ -140,8 +149,11 @@ _.extend(Deps.Computation.prototype, {
|
||||
|
||||
// callbacks can't add callbacks, because
|
||||
// self.invalidated === true.
|
||||
for(var i = 0, f; f = self._onInvalidateCallbacks[i]; i++)
|
||||
f(); // already bound with self as argument
|
||||
for(var i = 0, f; f = self._onInvalidateCallbacks[i]; i++) {
|
||||
Deps.nonreactive(function () {
|
||||
callWithNoYieldsAllowed(f, self);
|
||||
});
|
||||
}
|
||||
self._onInvalidateCallbacks = [];
|
||||
}
|
||||
},
|
||||
@@ -163,7 +175,7 @@ _.extend(Deps.Computation.prototype, {
|
||||
var previousInCompute = inCompute;
|
||||
inCompute = true;
|
||||
try {
|
||||
self._func(self);
|
||||
callWithNoYieldsAllowed(self._func, self);
|
||||
} finally {
|
||||
setCurrentComputation(previous);
|
||||
inCompute = false;
|
||||
@@ -309,9 +321,7 @@ _.extend(Deps, {
|
||||
throw new Error('Deps.autorun requires a function argument');
|
||||
|
||||
constructingComputation = true;
|
||||
var c = new Deps.Computation(function (c) {
|
||||
Meteor._noYieldsAllowed(function () { f(c); });
|
||||
}, Deps.currentComputation);
|
||||
var c = new Deps.Computation(f, Deps.currentComputation);
|
||||
|
||||
if (Deps.active)
|
||||
Deps.onInvalidate(function () {
|
||||
|
||||
@@ -457,18 +457,23 @@ DomUtils.compareElementIndex = function (a, b) {
|
||||
//
|
||||
// `frag` is a DocumentFragment and will be modified in
|
||||
// place. `container` is a DOM element.
|
||||
//
|
||||
// Returns the number of levels of wrapping applied, which is
|
||||
// 0 if no wrapping was performed.
|
||||
DomUtils.wrapFragmentForContainer = function (frag, container) {
|
||||
if (container && container.nodeName === "TABLE" &&
|
||||
_.any(frag.childNodes,
|
||||
function (n) { return n.nodeName === "TR"; })) {
|
||||
// Avoid putting a TR directly in a TABLE without an
|
||||
// intervening TBODY, because it doesn't work in IE. We do
|
||||
// the same thing on all browsers for ease of testing
|
||||
// intervening TBODY, because it doesn't work in (old?) IE.
|
||||
// We do the same thing on all browsers for ease of testing
|
||||
// and debugging.
|
||||
var tbody = document.createElement("TBODY");
|
||||
tbody.appendChild(frag);
|
||||
frag.appendChild(tbody);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Return true if `node` is part of the global DOM document. Like
|
||||
@@ -552,3 +557,17 @@ DomUtils.getElementValue = function (node) {
|
||||
return node.value;
|
||||
}
|
||||
};
|
||||
|
||||
DomUtils.extractRange = function (start, end, optContainer) {
|
||||
var parent = start.parentNode;
|
||||
var before = start.previousSibling;
|
||||
var after = end.nextSibling;
|
||||
var n;
|
||||
while ((n = (before ? before.nextSibling : parent.firstChild)) &&
|
||||
(n !== after)) {
|
||||
if (optContainer)
|
||||
optContainer.appendChild(n);
|
||||
else
|
||||
parent.removeChild(n);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</li>
|
||||
<li>
|
||||
If necessary, "Create Project"
|
||||
</li>
|
||||
<li>
|
||||
Click "APIs & auth" and "Credentials" on the left
|
||||
</li>
|
||||
|
||||
1
packages/handlebars/.npm/package/.gitignore
vendored
1
packages/handlebars/.npm/package/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
node_modules
|
||||
@@ -1,7 +0,0 @@
|
||||
This directory and the files immediately inside it are automatically generated
|
||||
when you change this package's NPM dependencies. Commit the files in this
|
||||
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
|
||||
so that others run the same versions of sub-dependencies.
|
||||
|
||||
You should NOT check in the node_modules directory that Meteor automatically
|
||||
creates; if you are using git, the .gitignore file tells git to ignore it.
|
||||
17
packages/handlebars/.npm/package/npm-shrinkwrap.json
generated
17
packages/handlebars/.npm/package/npm-shrinkwrap.json
generated
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"handlebars": {
|
||||
"version": "https://github.com/meteor/handlebars.js/tarball/543ec6689cf663cfda2d8f26c3c27de40aad7bd5",
|
||||
"dependencies": {
|
||||
"optimist": {
|
||||
"version": "0.3.7",
|
||||
"dependencies": {
|
||||
"wordwrap": {
|
||||
"version": "0.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
Handlebars = {};
|
||||
|
||||
// XXX we probably forgot to implement the #foo case where foo is not
|
||||
// a helper (and similarly the ^foo case)
|
||||
|
||||
// XXX there is a ton of stuff that needs testing! like,
|
||||
// everything. including the '..' stuff.
|
||||
|
||||
Handlebars.json_ast_to_func = function (ast) {
|
||||
return function (data, options) {
|
||||
return Handlebars.evaluate(ast, data, options);
|
||||
};
|
||||
};
|
||||
|
||||
// If minimongo is available (it's a weak dependency) use its ID stringifier to
|
||||
// label branches (so that, eg, ObjectId and strings don't overlap). Otherwise
|
||||
// just use the identity function.
|
||||
var idStringify = Package.minimongo
|
||||
? Package.minimongo.LocalCollection._idStringify
|
||||
: function (id) { return id; };
|
||||
|
||||
// block helpers take:
|
||||
// (N args), options (hash args, plus 'fn' and 'inverse')
|
||||
// and return text
|
||||
//
|
||||
// normal helpers take:
|
||||
// (N args), options (hash args)
|
||||
//
|
||||
// partials take one argument, data
|
||||
|
||||
// XXX handlebars' format for arguments is not the clearest, likely
|
||||
// for backwards compatibility to mustache. eg, options ===
|
||||
// options.fn. take the opportunity to clean this up. treat block
|
||||
// arguments (fn, inverse) as just another kind of argument, same as
|
||||
// what is passed in via named arguments.
|
||||
Handlebars._default_helpers = {
|
||||
'with': function (data, options) {
|
||||
if (!data || (data instanceof Array && !data.length))
|
||||
return options.inverse(this);
|
||||
else
|
||||
return options.fn(data);
|
||||
},
|
||||
'each': function (data, options) {
|
||||
var parentData = this;
|
||||
if (data && data.length > 0)
|
||||
return _.map(data, function(x, i) {
|
||||
// infer a branch key from the data
|
||||
var branch = ((x && x._id && idStringify(x._id)) ||
|
||||
(typeof x === 'string' ? x : null) ||
|
||||
Spark.UNIQUE_LABEL);
|
||||
return Spark.labelBranch(branch, function() {
|
||||
return options.fn(x);
|
||||
});
|
||||
}).join('');
|
||||
else
|
||||
return Spark.labelBranch(
|
||||
'else',
|
||||
function () {
|
||||
return options.inverse(parentData);
|
||||
});
|
||||
},
|
||||
'if': function (data, options) {
|
||||
if (!data || (data instanceof Array && !data.length))
|
||||
return options.inverse(this);
|
||||
else
|
||||
return options.fn(this);
|
||||
},
|
||||
'unless': function (data, options) {
|
||||
if (!data || (data instanceof Array && !data.length))
|
||||
return options.fn(this);
|
||||
else
|
||||
return options.inverse(this);
|
||||
}
|
||||
};
|
||||
|
||||
Handlebars.registerHelper = function (name, func) {
|
||||
if (name in Handlebars._default_helpers)
|
||||
throw new Error("There is already a helper '" + name + "'");
|
||||
Handlebars._default_helpers[name] = func;
|
||||
};
|
||||
|
||||
// Utility to HTML-escape a string.
|
||||
Handlebars._escape = (function() {
|
||||
var escape_map = {
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"`": "`", /* IE allows backtick-delimited attributes?? */
|
||||
"&": "&"
|
||||
};
|
||||
var escape_one = function(c) {
|
||||
return escape_map[c];
|
||||
};
|
||||
|
||||
return function (x) {
|
||||
return x.replace(/[&<>"'`]/g, escape_one);
|
||||
};
|
||||
})();
|
||||
|
||||
// be able to recognize default "this", which is different in different environments
|
||||
Handlebars._defaultThis = (function() { return this; })();
|
||||
|
||||
Handlebars.evaluate = function (ast, data, options) {
|
||||
options = options || {};
|
||||
var helpers = _.extend({}, Handlebars._default_helpers);
|
||||
_.extend(helpers, options.helpers || {});
|
||||
var partials = options.partials || {};
|
||||
|
||||
// re 'stack' arguments: top of stack is the current data to use for
|
||||
// the template. higher levels are the data referenced by
|
||||
// identifiers with one or more '..' segments. we have to keep the
|
||||
// stack pure-functional style, with a tree rather than an array,
|
||||
// because we want to continue to allow block helpers provided by
|
||||
// the user to capture their subtemplate rendering functions and
|
||||
// call them later, after we've finished running (for eg findLive.)
|
||||
// maybe revisit later.
|
||||
|
||||
var eval_value = function (stack, id) {
|
||||
if (typeof(id) !== "object")
|
||||
return id;
|
||||
|
||||
// follow '..' in {{../../foo.bar}}
|
||||
for (var i = 0; i < id[0]; i++) {
|
||||
if (!stack.parent)
|
||||
throw new Error("Too many '..' segments");
|
||||
else
|
||||
stack = stack.parent;
|
||||
}
|
||||
|
||||
if (id.length === 1)
|
||||
// no name: {{this}}, {{..}}, {{../..}}
|
||||
return stack.data;
|
||||
|
||||
var scopedToContext = false;
|
||||
if (id[1] === '') {
|
||||
// an empty path segment is our AST's way of encoding
|
||||
// the presence of 'this.' at the beginning of the path.
|
||||
id = id.slice();
|
||||
id.splice(1, 1); // remove the ''
|
||||
scopedToContext = true;
|
||||
}
|
||||
|
||||
// when calling functions (helpers/methods/getters), dataThis
|
||||
// tracks what to use for `this`. For helpers, it's the
|
||||
// current data context. For getters and methods on the data
|
||||
// context object, and on the return value of a helper, it's
|
||||
// the object where we got the getter or method.
|
||||
var dataThis = stack.data;
|
||||
|
||||
var data;
|
||||
if (id[0] === 0 && helpers.hasOwnProperty(id[1]) && ! scopedToContext) {
|
||||
// first path segment is a helper
|
||||
data = helpers[id[1]];
|
||||
} else {
|
||||
if ((! data instanceof Object) &&
|
||||
(typeof (function() {})[id[1]] !== 'undefined') &&
|
||||
! scopedToContext) {
|
||||
// Give a helpful error message if the user tried to name
|
||||
// a helper 'name', 'length', or some other built-in property
|
||||
// of function objects. Unfortunately, this case is very
|
||||
// hard to detect, as Template.foo.name = ... will fail silently,
|
||||
// and {{name}} will be silently empty if the property doesn't
|
||||
// exist (per Handlebars rules).
|
||||
// However, if there is no data context at all, we jump in.
|
||||
throw new Error("Can't call a helper '"+id[1]+"' because "+
|
||||
"it is a built-in function property in JavaScript");
|
||||
}
|
||||
// first path segment is property of data context
|
||||
data = (stack.data && stack.data[id[1]]);
|
||||
}
|
||||
|
||||
// handle dots, as in {{foo.bar}}
|
||||
for (var i = 2; i < id.length; i++) {
|
||||
// Call functions when taking the dot, to support
|
||||
// for example currentUser.name.
|
||||
//
|
||||
// In the case of {{foo.bar}}, we end up returning one of:
|
||||
// - helpers.foo.bar
|
||||
// - helpers.foo().bar
|
||||
// - stack.data.foo.bar
|
||||
// - stack.data.foo().bar.
|
||||
//
|
||||
// The caller does the final application with any
|
||||
// arguments, as in {{foo.bar arg1 arg2}}, and passes
|
||||
// the current data context in `this`. Therefore,
|
||||
// we use the current data context (`helperThis`)
|
||||
// for all function calls.
|
||||
if (typeof data === 'function') {
|
||||
data = data.call(dataThis);
|
||||
dataThis = data;
|
||||
}
|
||||
if (data === undefined || data === null) {
|
||||
// Handlebars fails silently and returns "" if
|
||||
// we start to access properties that don't exist.
|
||||
data = '';
|
||||
break;
|
||||
}
|
||||
|
||||
data = data[id[i]];
|
||||
}
|
||||
|
||||
// ensure `this` is bound appropriately when the caller
|
||||
// invokes `data` with any arguments. For example,
|
||||
// in {{foo.bar baz}}, the caller must supply `baz`,
|
||||
// but we alone have `foo` (in `dataThis`).
|
||||
if (typeof data === 'function')
|
||||
return _.bind(data, dataThis);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// 'extra' will be clobbered, but not 'params'.
|
||||
// if (isNested), evaluate params.slice(1) as a nested
|
||||
// helper invocation if there is at least one positional
|
||||
// argument. This is used for block helpers.
|
||||
var invoke = function (stack, params, extra, isNested) {
|
||||
extra = extra || {};
|
||||
params = params.slice(0);
|
||||
|
||||
// remove hash (dictionary of keyword arguments) from
|
||||
// the end of params, if present.
|
||||
var last = params[params.length - 1];
|
||||
var hash = {};
|
||||
if (typeof(last) === "object" && !(last instanceof Array)) {
|
||||
// evaluate hash values, which are found as invocations
|
||||
// like [0, "foo"]
|
||||
_.each(params.pop(), function(v,k) {
|
||||
var result = eval_value(stack, v);
|
||||
hash[k] = (typeof result === "function" ? result() : result);
|
||||
});
|
||||
}
|
||||
|
||||
var apply = function (values, extra) {
|
||||
var args = values.slice(1);
|
||||
for(var i=0; i<args.length; i++)
|
||||
if (typeof args[i] === "function")
|
||||
args[i] = args[i](); // `this` already bound by eval_value
|
||||
if (extra)
|
||||
args.push(extra);
|
||||
return values[0].apply(stack.data, args);
|
||||
};
|
||||
|
||||
var values = new Array(params.length);
|
||||
for(var i=0; i<params.length; i++)
|
||||
values[i] = eval_value(stack, params[i]);
|
||||
|
||||
if (typeof(values[0]) !== "function")
|
||||
return values[0];
|
||||
|
||||
if (isNested && values.length > 1) {
|
||||
// at least one positional argument; not no args
|
||||
// or only hash args.
|
||||
var oneArg = values[1];
|
||||
if (typeof oneArg === "function")
|
||||
// invoke the positional arguments
|
||||
// (and hash arguments) as a nested helper invocation.
|
||||
oneArg = apply(values.slice(1), {hash:hash});
|
||||
values = [values[0], oneArg];
|
||||
// keyword args don't go to the block helper, then.
|
||||
extra.hash = {};
|
||||
} else {
|
||||
extra.hash = hash;
|
||||
}
|
||||
|
||||
return apply(values, extra);
|
||||
};
|
||||
|
||||
var template = function (stack, elts, basePCKey) {
|
||||
var buf = [];
|
||||
|
||||
var toString = function (x) {
|
||||
if (typeof x === "string") return x;
|
||||
// May want to revisit the following one day
|
||||
if (x === null) return "null";
|
||||
if (x === undefined) return "";
|
||||
return x.toString();
|
||||
};
|
||||
|
||||
// wrap `fn` and `inverse` blocks in chunks having `data`, if the data
|
||||
// is different from the enclosing data, so that the data is available
|
||||
// at runtime for events.
|
||||
var decorateBlockFn = function(fn, old_data) {
|
||||
return function(data) {
|
||||
// don't create spurious annotations when data is same
|
||||
// as before (or when transitioning between e.g. `window` and
|
||||
// `undefined`)
|
||||
if ((data || Handlebars._defaultThis) ===
|
||||
(old_data || Handlebars._defaultThis))
|
||||
return fn(data);
|
||||
else
|
||||
return Spark.setDataContext(data, fn(data));
|
||||
};
|
||||
};
|
||||
|
||||
// Handle the return value of a {{helper}}.
|
||||
// Takes a:
|
||||
// string - escapes it
|
||||
// SafeString - returns the underlying string unescaped
|
||||
// other value - coerces to a string and escapes it
|
||||
var maybeEscape = function(x) {
|
||||
if (x instanceof Handlebars.SafeString)
|
||||
return x.toString();
|
||||
return Handlebars._escape(toString(x));
|
||||
};
|
||||
|
||||
var curIndex;
|
||||
// Construct a unique key for the current position
|
||||
// in the AST. Since template(...) is invoked recursively,
|
||||
// the "PC" (program counter) key is hierarchical, consisting
|
||||
// of one or more numbers, for example '0' or '1.3.0.1'.
|
||||
var getPCKey = function() {
|
||||
return (basePCKey ? basePCKey+'.' : '') + curIndex;
|
||||
};
|
||||
var branch = function(name, func) {
|
||||
// Construct a unique branch identifier based on what partial
|
||||
// we're in, what partial or helper we're calling, and our index
|
||||
// into the template AST (essentially the program counter).
|
||||
// If "foo" calls "bar" at index 3, it looks like: bar@foo#3.
|
||||
return Spark.labelBranch(name + "@" + getPCKey(), func);
|
||||
};
|
||||
|
||||
_.each(elts, function (elt, index) {
|
||||
curIndex = index;
|
||||
if (typeof(elt) === "string")
|
||||
buf.push(elt);
|
||||
else if (elt[0] === '{')
|
||||
// {{double stache}}
|
||||
buf.push(branch(elt[1], function () {
|
||||
return maybeEscape(invoke(stack, elt[1]));
|
||||
}));
|
||||
else if (elt[0] === '!')
|
||||
// {{{triple stache}}}
|
||||
buf.push(branch(elt[1], function () {
|
||||
return toString(invoke(stack, elt[1] || ''));
|
||||
}));
|
||||
else if (elt[0] === '#') {
|
||||
// {{#block helper}}
|
||||
var pcKey = getPCKey();
|
||||
var block = decorateBlockFn(
|
||||
function (data) {
|
||||
return template({parent: stack, data: data}, elt[2], pcKey);
|
||||
}, stack.data);
|
||||
block.fn = block;
|
||||
block.inverse = decorateBlockFn(
|
||||
function (data) {
|
||||
return template({parent: stack, data: data}, elt[3] || [], pcKey);
|
||||
}, stack.data);
|
||||
var html = branch(elt[1], function () {
|
||||
return toString(invoke(stack, elt[1], block, true));
|
||||
});
|
||||
buf.push(html);
|
||||
} else if (elt[0] === '>') {
|
||||
// {{> partial}}
|
||||
var partialName = elt[1];
|
||||
if (!(partialName in partials))
|
||||
// XXX why do we call these templates in docs and partials in code?
|
||||
throw new Error("No such template '" + partialName + "'");
|
||||
// call the partial
|
||||
var html = branch(partialName, function () {
|
||||
return toString(partials[partialName](stack.data));
|
||||
});
|
||||
buf.push(html);
|
||||
} else
|
||||
throw new Error("bad element in template");
|
||||
});
|
||||
|
||||
return buf.join('');
|
||||
};
|
||||
|
||||
// Set the prefix for PC keys, which identify call sites in the AST
|
||||
// for the purpose of chunk matching.
|
||||
// `options.name` will be null in the body, but otherwise have a value,
|
||||
// assuming `options` was assembled in templating/deftemplate.js.
|
||||
var rootPCKey = (options.name||"")+"#";
|
||||
|
||||
return template({data: data, parent: null}, ast, rootPCKey);
|
||||
};
|
||||
|
||||
Handlebars.SafeString = function(string) {
|
||||
this.string = string;
|
||||
};
|
||||
Handlebars.SafeString.prototype.toString = function() {
|
||||
return this.string.toString();
|
||||
};
|
||||
@@ -1,42 +1,14 @@
|
||||
Package.describe({
|
||||
summary: "Simple semantic templating language",
|
||||
summary: "Deprecated",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
// Fork of 1.0.7 dropping a used-only-by-bin/handlebars dependency on the very
|
||||
// large uglify-js 1.2.6.
|
||||
handlebars: 'https://github.com/meteor/handlebars.js/tarball/543ec6689cf663cfda2d8f26c3c27de40aad7bd5'
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('underscore');
|
||||
api.use('spark', 'client');
|
||||
|
||||
api.export('Handlebars');
|
||||
|
||||
|
||||
// If we have minimongo available, use its idStringify function.
|
||||
api.use('minimongo', 'client', {weak: true});
|
||||
|
||||
// XXX these should be split up into two different slices, not
|
||||
// different code with totally different APIs that is sent depending
|
||||
// on the architecture
|
||||
api.add_files('parse-handlebars.js', 'server');
|
||||
api.add_files('evaluate-handlebars.js', 'client');
|
||||
|
||||
// XXX This package has been folded into the 'templating' package
|
||||
// for now. Historically, you could see it in your package list
|
||||
// (because it didn't have internal: true, which it probably should
|
||||
// have), but adding it didn't do anything (because it just
|
||||
// contained the handlebars precompiler and runtime, not any
|
||||
// functions you could call yourself.) So leave it around as an
|
||||
// empty package for the moment so as to not break the projects of
|
||||
// anyone that happened to type 'meteor add handlebars' because they
|
||||
// thought they had to.
|
||||
// XXX we unfortunately we can't do this since `meteor test-packages`
|
||||
// tries to load all packages.
|
||||
//
|
||||
// throw new Error(
|
||||
// "The 'handlebars' package is deprecated. "
|
||||
// + "`Handlebars.registerHelper` is now `UI.registerHelper` in the 'ui' package.");
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('underscore');
|
||||
});
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
Handlebars = {};
|
||||
|
||||
/* Our format:
|
||||
*
|
||||
* A 'template' is an array. Each element in it is either
|
||||
* - a literal string to echo
|
||||
* - an escaped substition: ['{', invocation]
|
||||
* - an unescaped substition: ['!', invocation]
|
||||
* - a (conditional or iterated) block:
|
||||
* ['#', invocation, template_a, template_b]
|
||||
* (the second template is optional)
|
||||
* - a partial: ['>', partial_name] (partial_name is a string)
|
||||
*
|
||||
* An 'invocation' is an array: one or more 'values', then an optional
|
||||
* hash (of which the keys are strings, and the values are 'values'.)
|
||||
*
|
||||
* An 'identifier' is:
|
||||
* - [depth, key, key, key..]
|
||||
* Eg, '../../a.b.c' would be [2, 'a', 'b', 'c']. 'a' would be [0, 'a'].
|
||||
* And 'this' or '.' would be [0].
|
||||
*
|
||||
* A 'value' is either an identifier, or a string, int, or bool.
|
||||
*
|
||||
* You should provide a block helper 'with' since we will emit calls
|
||||
* to it (if the user passes the second 'context' argument to a
|
||||
* partial.)
|
||||
*/
|
||||
|
||||
var path = Npm.require('path');
|
||||
var hbars = Npm.require('handlebars');
|
||||
|
||||
// Has keys 'message', 'line'
|
||||
Handlebars.ParseError = function (message, line) {
|
||||
this.message = message;
|
||||
if (line)
|
||||
this.line = line;
|
||||
};
|
||||
|
||||
// Raises Handlebars.ParseError if the Handlebars parser fails. We
|
||||
// will do our best to decode the output of Handlebars into a message
|
||||
// and a line number.
|
||||
|
||||
// If Handlebars parsing fails, the Handlebars parser error will
|
||||
// escape to the caller.
|
||||
//
|
||||
Handlebars.to_json_ast = function (code) {
|
||||
try {
|
||||
var ast = hbars.parse(code);
|
||||
} catch (e) {
|
||||
// The Handlebars parser throws Error objects with a message
|
||||
// attribute (and nothing else) and we must do our best. Parse
|
||||
// errors include a line number (relative to the start of 'code'
|
||||
// of course) which we'll attempt to parse out. (Handlebars
|
||||
// almost, but not quite copies the line number information onto
|
||||
// the Error object.) Other than parse errors, you also see very
|
||||
// short strings like "else doesn't match unless" (with no
|
||||
// location information.)
|
||||
var m = e.message.match(/^Parse error on line (\d+):([\s\S]*)$/)
|
||||
if (m)
|
||||
throw new Handlebars.ParseError("Parse error:" + m[2], +m[1]);
|
||||
|
||||
if (e.message)
|
||||
throw new Handlebars.ParseError(e.message);
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Recreate Handlebars.Exception to properly report error messages
|
||||
// and stack traces. (https://github.com/wycats/handlebars.js/issues/226)
|
||||
makeHandlebarsExceptionsVisible();
|
||||
|
||||
var identifier = function (node) {
|
||||
if (node.type !== "ID")
|
||||
throw new Error("got ast node " + node.type + " for identifier");
|
||||
// drop node.isScoped. this is true if there was a 'this' or '.'
|
||||
// anywhere in the path. vanilla handlebars will turn off
|
||||
// helpers lookup if isScoped is true, but this is too restrictive
|
||||
// for us.
|
||||
var ret = [node.depth];
|
||||
// we still want to turn off helper lookup if path starts with 'this.'
|
||||
// as in {{this.foo}}, which means it has to look different from {{foo}}
|
||||
// in our AST. signal the presence of 'this' in our AST using an empty
|
||||
// path segment.
|
||||
if (/^this\./.test(node.original))
|
||||
ret.push('');
|
||||
return ret.concat(node.parts);
|
||||
};
|
||||
|
||||
var value = function (node) {
|
||||
// Work around handlebars.js Issue #422 - Negative integers for
|
||||
// helpers get trapped as ID. handlebars doesn't support floating
|
||||
// point, just integers.
|
||||
if (node.type === 'ID' && /^-\d+$/.test(node.string)) {
|
||||
// Reconstruct node
|
||||
node.type = 'INTEGER';
|
||||
node.integer = node.string;
|
||||
}
|
||||
|
||||
var choices = {
|
||||
ID: function (node) {return identifier(node);},
|
||||
STRING: function (node) {return node.string;},
|
||||
INTEGER: function (node) {return +node.integer;},
|
||||
BOOLEAN: function (node) {return (node.bool === 'true');}
|
||||
};
|
||||
if (!(node.type in choices))
|
||||
throw new Error("got ast node " + node.type + " for value");
|
||||
return choices[node.type](node);
|
||||
};
|
||||
|
||||
var hash = function (node) {
|
||||
if (node.type !== "hash")
|
||||
throw new Error("got ast node " + node.type + " for hash");
|
||||
var ret = {};
|
||||
_.each(node.pairs, function (p) {
|
||||
ret[p[0]] = value(p[1]);
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
|
||||
var invocation = function (node) {
|
||||
if (node.type !== "mustache")
|
||||
throw new Error("got ast node " + node.type + " for invocation");
|
||||
var ret = [node.id];
|
||||
ret = ret.concat(node.params);
|
||||
ret = _.map(ret, value);
|
||||
if (node.hash)
|
||||
ret.push(hash(node.hash));
|
||||
return ret;
|
||||
};
|
||||
|
||||
var template = function (nodes) {
|
||||
var ret = [];
|
||||
|
||||
if (!nodes)
|
||||
return [];
|
||||
|
||||
var choices = {
|
||||
mustache: function (node) {
|
||||
ret.push([node.escaped ? '{' : '!', invocation(node)]);
|
||||
},
|
||||
partial: function (node) {
|
||||
var id = identifier(node.id);
|
||||
if (id.length !== 2 || id[0] !== 0)
|
||||
// XXX actually should just get the literal string the
|
||||
// entered, and avoid identifier parsing
|
||||
throw new Error("Template names shouldn't contain '.' or '/'");
|
||||
var x = ['>', id[1]];
|
||||
if (node.context)
|
||||
x = ['#', [[0, 'with'], identifier(node.context)], [x]];
|
||||
ret.push(x);
|
||||
},
|
||||
block: function (node) {
|
||||
var x = ['#', invocation(node.mustache),
|
||||
template(node.program.statements)];
|
||||
if (node.program.inverse)
|
||||
x.push(template(node.program.inverse.statements));
|
||||
ret.push(x);
|
||||
},
|
||||
inverse: function (node) {
|
||||
ret.push(['#', invocation(node.mustache),
|
||||
node.program.inverse &&
|
||||
template(node.program.inverse.statements) || [],
|
||||
template(node.program.statements)]);
|
||||
},
|
||||
content: function (node) {ret.push(node.string);},
|
||||
comment: function (node) {}
|
||||
};
|
||||
|
||||
_.each(nodes, function (node) {
|
||||
if (!(node.type in choices))
|
||||
throw new Error("got ast node " + node.type + " in template");
|
||||
choices[node.type](node);
|
||||
});
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
if (ast.type !== "program")
|
||||
throw new Error("got ast node " + node.type + " at toplevel");
|
||||
return template(ast.statements);
|
||||
};
|
||||
|
||||
var makeHandlebarsExceptionsVisible = function () {
|
||||
hbars.Exception = function(message) {
|
||||
this.message = message;
|
||||
// In Node, if we don't do this we don't see the message displayed
|
||||
// nor the right stack trace.
|
||||
Error.captureStackTrace(this, arguments.callee);
|
||||
};
|
||||
hbars.Exception.prototype = new Error();
|
||||
hbars.Exception.prototype.name = 'Handlebars.Exception';
|
||||
};
|
||||
1
packages/html-tools/.gitignore
vendored
Normal file
1
packages/html-tools/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
148
packages/html-tools/README.md
Normal file
148
packages/html-tools/README.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# html-tools
|
||||
|
||||
A lightweight HTML tokenizer and parser which outputs to the HTMLjs
|
||||
object representation. Special hooks allow the syntax to be extended
|
||||
to parse an HTML-like template language like Spacebars.
|
||||
|
||||
```
|
||||
HTML.parseFragment("<div class=greeting>Hello<br>World</div>")
|
||||
|
||||
=> HTML.DIV({'class':'greeting'}, "Hello", HTML.BR(), "World"))
|
||||
```
|
||||
|
||||
This package is used by the Spacebars compiler, which normally only
|
||||
runs at bundle time but can also be used at runtime on the client or
|
||||
server.
|
||||
|
||||
## Invoking the Parser
|
||||
|
||||
`HTML.parseFragment(input, options)` - Takes an input string or Scanner object and returns HTMLjs.
|
||||
|
||||
In the basic case, where no options are passed, `parseFragment` will consume the entire input (the full string or the rest of the Scanner).
|
||||
|
||||
The options are as follows:
|
||||
|
||||
#### getSpecialTag
|
||||
|
||||
This option extends the HTML parser to parse template tags such as `{{foo}}`.
|
||||
|
||||
`getSpecialTag: function (scanner, templateTagPosition) { ... }` - A function for the parser to call after every HTML token and at various positions within tags. If the function returns a non-null value, that value is wrapped in an `HTML.Special` node which is inserted into the HTMLjs tree at the appropriate location. The function is expected to advance the scanner if it succeeds at parsing a template tag (see the section on `HTML.Scanner`).
|
||||
|
||||
There are four possible outcomes when `getSpecialTag` is called:
|
||||
|
||||
* Not a template tag - Leave the scanner as is, and return `null`. A quick peek at the next character should bail to this case if the start of a template tag is not seen.
|
||||
* Bad template tag - Call `scanner.fatal`, which aborts parsing completely. Once the beginning of a template tag is seen, `getSpecialTag` will generally want to commit, and either succeed or fail trying).
|
||||
* Good template tag - Advance the scanner to the end of the template tag and return an object.
|
||||
* Comment tag - Advance the scanner and return `null`. For example, a Spacebars comment is `{{! foo}}`.
|
||||
|
||||
The `templateTagPosition` argument to `getSpecialTag` is one of:
|
||||
|
||||
* `HTML.TEMPLATE_TAG_POSITION.ELEMENT` - At "element level," meaning somewhere an HTML tag could be.
|
||||
* `HTML.TEMPLATE_TAG_POSITION.IN_START_TAG` - Inside a start tag, as in `<div {{foo}}>`, where you might otherwise find `name=value`.
|
||||
* `HTML.TEMPLATE_TAG_POSITION.IN_ATTRIBUTE` - Inside the value of an HTML attribute, as in `<div class={{foo}}>`.
|
||||
* `HTML.TEMPLATE_TAG_POSITION.IN_RCDATA` - Inside a TEXTAREA or a block helper inside an attribute, where character references are allowed ("replaced character data") but not tags.
|
||||
* `HTML.TEMPLATE_TAG_POSITION.IN_RAWTEXT` - In a context where character references are not parsed, such as a script tag, style tag, or markdown helper.
|
||||
|
||||
It's completely normal for `getSpecialTag` to invoke `HTML.parseFragment` recursively on the same scanner (see `shouldStop`). If it does so, the same value of `getSpecialTag` must be passed to the second invocation.
|
||||
|
||||
At the moment, template tags must begin with `{`. The parser does not try calling `getSpecialTag` for every character of an HTML document, only at token boundaries, and it knows to always end a token at `{`.
|
||||
|
||||
**XXX Better error message for `<div {{k}}={{v}}>`.**
|
||||
|
||||
**XXX Do something with `<input type=checkbox {{#if foo}}checked{{/if}}>`**
|
||||
|
||||
**XXX Why both IN_ATTRIBUTE and IN_RCDATA?**
|
||||
|
||||
**XXX Fix Markdown**
|
||||
|
||||
#### textMode
|
||||
|
||||
The `textMode` option, if present, causes the parser to parse text (such as the contents of a `<textarea>` tag or part of an attribute) instead of HTML. In a text mode, for example, the input `"<"` is not a parse error (because a bare `<` is allowed in a textarea or attribute).
|
||||
|
||||
The value of `textMode` must be one of:
|
||||
|
||||
* `HTML.TEXTMODE.RCDATA` - Interpret character references (the usual case)
|
||||
* `HTML.TEXTMODE.STRING` - Don't interpret character references (the RAWTEXT case)
|
||||
|
||||
#### shouldStop
|
||||
|
||||
`shouldStop: function (scanner) { ... }` - A function that the parser invokes between tokens to check whether it should stop parsing. The function should return a boolean value.
|
||||
|
||||
The `shouldStop` function provides a way to put a "wall" in the input stream for the purpose of parsing HTML content embedded in a template tag. For example, take the template `{{#if happy}}yay{{/if}}`. The scanner will be advanced to the start of the word `yay` before `parseFragment` is called to parse the contents of the tag. (Note that the caller happens to be the `getSpecialTag` function of an enclosing `parseFragment`.) When parsing from `yay`, the `shouldStop` function is used to end the fragment at `{{/if}}`, which, like `{{/blah}}` or `{{else}}`, couldn't possibly be actual content that belongs in the fragment. Even if HTML tags are not closed, as in the malformed template `{{#if foo}}<div>{{else}}`, the fragment stops at the `{{else}}`, and the error is an unclosed `<div>` (before the parser notices the unclosed `{{#if}}`).
|
||||
|
||||
**XXX This option doesn't seem very elegant, or at least the way it's passed around internally isn't.**
|
||||
|
||||
## HTML.Scanner class
|
||||
|
||||
To write `getSpecialTag` and `shouldStop` functions, you have to
|
||||
interface with the `HTML.Scanner` class used by html-tools. It's a
|
||||
general class that could be used by any parser/lexer/tokenizer.
|
||||
|
||||
A Scanner has an immutable source document and a mutable pointer into
|
||||
the document.
|
||||
|
||||
* `new Scanner(input)` - constructs a Scanner with source string `input`
|
||||
* `scanner.input` (read-only) - the entire source string
|
||||
* `scanner.pos` (read/write) - the current index into the source string
|
||||
|
||||
Scanners provide these methods for convenience:
|
||||
|
||||
* `scanner.rest()` - `input.slice(pos)` (the rest of the document)
|
||||
* `scanner.peek()` - `input.charAt(pos)` (the next character)
|
||||
* `scanner.isEOF()` - true if `pos` is at or beyond the end of `input`
|
||||
* `scanner.fatal(msg)` - throw an error indicating a problem at `pos`
|
||||
|
||||
Even though `scanner.rest()` performs a substring operation, it should be considered fast and O(1), because all known JavaScript runtimes in use have constant-time substring. It would be possible, but extremely clumsy, to avoid such a substring operation while performing the usual business of a parser, which is to try to match a regex anchored at a particular index.
|
||||
|
||||
Functions that take scanners generally have three possible outcomes:
|
||||
|
||||
* Success: Advance `scanner.pos` and return some truthy value
|
||||
* Failure: Leave `scanner.pos` alone and return `null`
|
||||
* Fatal: Throw an exception via `scanner.fatal`
|
||||
|
||||
It's particularly important that in the Failure case, the function restores the scanner to the state it found it. This makes it possible to immediately try another parse function when one fails and form alternations such as `foo(scanner) || bar(scanner)`.
|
||||
|
||||
It's often easiest to avoid the Failure case altogether, writing parse functions that always succeed or throw. This requires less bookkeeping and leads to good error messages. A Failure case may be added if it is simple to check for up front and makes the function easier to use in an alternation. We say a function has "committed" or "will succeed or fail fatally trying" when it has reached a point where it must return a value or throw. Any parse function that has moved the scanner position and not remembered the original position is necessarily committed. Usually, committing is completely natural in the context of the language being parsed; for example, `{{` in a template always starts a template tag or throws an error about a malformed template tag.
|
||||
|
||||
## HTML Dialect
|
||||
|
||||
HTML has many dialects and potential degrees of permissiveness. We
|
||||
use the WHATWG syntax spec and are pretty strict, failing on any
|
||||
"parse error" cases, which basically means the input has to be
|
||||
valid "HTML5" (except for the template tags).
|
||||
|
||||
HTML syntax references:
|
||||
|
||||
* [Human-readable syntax guide](http://developers.whatwg.org/syntax.html#syntax)
|
||||
* [Tokenization state machine](http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html)
|
||||
|
||||
The WHATWG parser without error recovery is strict compared to
|
||||
browsers (which will recover from almost anything), but lenient
|
||||
compared to the now-defunct XHTML spec (which required lowercase tag
|
||||
names and lots more escaping of special characters).
|
||||
|
||||
The following are examples of **errors**:
|
||||
|
||||
* A stray or unclosed `<` character
|
||||
* An unknown character reference like `&asdf;`
|
||||
* Self-closing tags like `<div/>` (except for BR, HR, INPUT, and other "void" elements)
|
||||
* End tags for void elements (BR, HR, INPUT, etc.)
|
||||
* Missing end tags, in most cases (e.g. missing `</div>`)
|
||||
|
||||
The following are **permitted**:
|
||||
|
||||
* Bare `>` characters
|
||||
* Bare `&` that can't be confused with a character reference
|
||||
* Uppercase or lowercase tag and attribute names (case insensitive)
|
||||
* Unquoted and valueless attributes - `<input type=checkbox checked>`
|
||||
* Most characters in attribute values - `<img alt=x,y>`
|
||||
* Embedded SVG elements
|
||||
|
||||
**XXX Currently you have to close your Ps, LIs, and other tags for which the spec allows the end tag to be omitted in many cases**
|
||||
|
||||
## Character References
|
||||
|
||||
This package contains a lookup table for all known named character references in HTML, of which there are over 2,000, from `Á` (capital A, acute accent) to `‌` (zero-width non-joiner), as well as code for interpreting numeric character entities like `A`.
|
||||
|
||||
Since character references are parsed into `HTML.CharRef` objects which contain both the raw and interpreted form, we never have to convert between the forms except at parse time.
|
||||
|
||||
2414
packages/html-tools/charref.js
Normal file
2414
packages/html-tools/charref.js
Normal file
File diff suppressed because it is too large
Load Diff
114
packages/html-tools/charref_tests.js
Normal file
114
packages/html-tools/charref_tests.js
Normal file
@@ -0,0 +1,114 @@
|
||||
var Scanner = HTMLTools.Scanner;
|
||||
var getCharacterReference = HTMLTools.Parse.getCharacterReference;
|
||||
|
||||
Tinytest.add("html-tools - entities", function (test) {
|
||||
var succeed = function (input, match, codepoints) {
|
||||
if (typeof input === 'string')
|
||||
input = {input: input};
|
||||
|
||||
// match arg is optional; codepoints is never a string
|
||||
if (typeof match !== 'string') {
|
||||
codepoints = match;
|
||||
match = input.input;
|
||||
}
|
||||
|
||||
var scanner = new Scanner(input.input);
|
||||
var result = getCharacterReference(scanner, input.inAttribute, input.allowedChar);
|
||||
test.isTrue(result);
|
||||
test.equal(scanner.pos, match.length);
|
||||
test.equal(result, {
|
||||
t: 'CharRef',
|
||||
v: match,
|
||||
cp: _.map(codepoints,
|
||||
function (x) { return (typeof x === 'string' ?
|
||||
x.charCodeAt(0) : x); })
|
||||
});
|
||||
};
|
||||
|
||||
var ignore = function (input) {
|
||||
if (typeof input === 'string')
|
||||
input = {input: input};
|
||||
|
||||
var scanner = new Scanner(input.input);
|
||||
var result = getCharacterReference(scanner, input.inAttribute, input.allowedChar);
|
||||
test.isFalse(result);
|
||||
test.equal(scanner.pos, 0);
|
||||
};
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
if (typeof input === 'string')
|
||||
input = {input: input};
|
||||
|
||||
var scanner = new Scanner(input.input);
|
||||
var error;
|
||||
try {
|
||||
getCharacterReference(scanner, input.inAttribute, input.allowedChar);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (error)
|
||||
test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
ignore('a');
|
||||
ignore('&');
|
||||
ignore('&&');
|
||||
ignore('&\t');
|
||||
ignore('& ');
|
||||
fatal('&#', 'Invalid numerical character reference starting with &#');
|
||||
ignore('&a');
|
||||
fatal('&a;', 'Invalid character reference: &a;');
|
||||
ignore({input: '&"', allowedChar: '"'});
|
||||
ignore('&"');
|
||||
|
||||
succeed('>', ['>']);
|
||||
fatal('>', 'Character reference requires semicolon');
|
||||
ignore('&aaa');
|
||||
fatal('>a', 'Character reference requires semicolon');
|
||||
ignore({input: '>a', inAttribute: true});
|
||||
fatal({input: '>=', inAttribute: true}, 'Character reference requires semicolon: >');
|
||||
|
||||
succeed('>;', '>', ['>']);
|
||||
|
||||
fatal('&asdflkj;', 'Invalid character reference: &asdflkj;');
|
||||
fatal('&A0asdflkj;', 'Invalid character reference: &A0asdflkj;');
|
||||
ignore('&A0asdflkj');
|
||||
|
||||
succeed('𝕫', [120171]);
|
||||
succeed('∾̳', [8766, 819]);
|
||||
|
||||
succeed(' ', [10]);
|
||||
fatal('
', 'Invalid numerical character reference starting with &#');
|
||||
fatal('&#xg;', 'Invalid numerical character reference starting with &#');
|
||||
fatal('&#;', 'Invalid numerical character reference starting with &#');
|
||||
fatal('&#a;', 'Invalid numerical character reference starting with &#');
|
||||
fatal('&#a', 'Invalid numerical character reference starting with &#');
|
||||
fatal('&#z', 'Invalid numerical character reference starting with &#');
|
||||
succeed('
', [10]);
|
||||
fatal('�', 'Numerical character reference too large: 1000000000010');
|
||||
succeed('
', [10]);
|
||||
fatal('�', 'Numerical character reference too large: 0x100000000000a');
|
||||
succeed('
', [10]);
|
||||
succeed('
', [10]);
|
||||
succeed('
', [10]);
|
||||
succeed('
', [10]);
|
||||
succeed('
', [10]);
|
||||
|
||||
fatal('�', 'Illegal codepoint in numerical character reference: �');
|
||||
fatal('�', 'Illegal codepoint in numerical character reference: �');
|
||||
|
||||
fatal('', 'Illegal codepoint in numerical character reference: ');
|
||||
succeed('', [12]);
|
||||
fatal('', 'Illegal codepoint in numerical character reference: ');
|
||||
succeed('', [12]);
|
||||
|
||||
fatal('', 'Illegal codepoint in numerical character reference');
|
||||
fatal('', 'Illegal codepoint in numerical character reference');
|
||||
succeed('􏿽', [0x10fffd]);
|
||||
|
||||
fatal('', 'Illegal codepoint in numerical character reference');
|
||||
fatal('', 'Illegal codepoint in numerical character reference');
|
||||
succeed('􏿽', [0x10fffd]);
|
||||
|
||||
});
|
||||
26
packages/html-tools/package.js
Normal file
26
packages/html-tools/package.js
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
Package.describe({
|
||||
summary: "Standards-compliant HTML tools"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('htmljs');
|
||||
|
||||
api.export('HTMLTools');
|
||||
|
||||
api.add_files(['utils.js',
|
||||
'scanner.js',
|
||||
'charref.js',
|
||||
'tokenize.js',
|
||||
'parse.js']);
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('html-tools');
|
||||
api.use('underscore');
|
||||
api.use('spacebars-compiler'); // for `HTML.toJS`
|
||||
api.add_files(['charref_tests.js',
|
||||
'tokenize_tests.js',
|
||||
'parse_tests.js']);
|
||||
});
|
||||
340
packages/html-tools/parse.js
Normal file
340
packages/html-tools/parse.js
Normal file
@@ -0,0 +1,340 @@
|
||||
|
||||
HTMLTools.Special = function (value) {
|
||||
if (! (this instanceof HTMLTools.Special))
|
||||
// called without `new`
|
||||
return new HTMLTools.Special(value);
|
||||
|
||||
this.value = value;
|
||||
};
|
||||
HTMLTools.Special.prototype.toJS = function (options) {
|
||||
// XXX this is weird because toJS is defined in spacebars-compiler.
|
||||
// Think about where HTMLTools.Special and toJS should go.
|
||||
return HTML.Tag.prototype.toJS.call({tagName: 'HTMLTools.Special',
|
||||
attrs: this.value,
|
||||
children: []},
|
||||
options);
|
||||
};
|
||||
|
||||
HTMLTools.parseFragment = function (input, options) {
|
||||
var scanner;
|
||||
if (typeof input === 'string')
|
||||
scanner = new Scanner(input);
|
||||
else
|
||||
// input can be a scanner. We'd better not have a different
|
||||
// value for the "getSpecialTag" option as when the scanner
|
||||
// was created, because we don't do anything special to reset
|
||||
// the value (which is attached to the scanner).
|
||||
scanner = input;
|
||||
|
||||
// ```
|
||||
// { getSpecialTag: function (scanner, templateTagPosition) {
|
||||
// if (templateTagPosition === HTMLTools.TEMPLATE_TAG_POSITION.ELEMENT) {
|
||||
// ...
|
||||
// ```
|
||||
if (options && options.getSpecialTag)
|
||||
scanner.getSpecialTag = options.getSpecialTag;
|
||||
|
||||
// function (scanner) -> boolean
|
||||
var shouldStop = options && options.shouldStop;
|
||||
|
||||
var result;
|
||||
if (options && options.textMode) {
|
||||
if (options.textMode === HTML.TEXTMODE.STRING) {
|
||||
result = getRawText(scanner, null, shouldStop);
|
||||
} else if (options.textMode === HTML.TEXTMODE.RCDATA) {
|
||||
result = getRCData(scanner, null, shouldStop);
|
||||
} else {
|
||||
throw new Error("Unsupported textMode: " + options.textMode);
|
||||
}
|
||||
} else {
|
||||
result = getContent(scanner, shouldStop);
|
||||
}
|
||||
if (! scanner.isEOF()) {
|
||||
var posBefore = scanner.pos;
|
||||
|
||||
try {
|
||||
var endTag = getHTMLToken(scanner);
|
||||
} catch (e) {
|
||||
// ignore errors from getSpecialTag
|
||||
}
|
||||
|
||||
// XXX we make some assumptions about shouldStop here, like that it
|
||||
// won't tell us to stop at an HTML end tag. Should refactor
|
||||
// `shouldStop` into something more suitable.
|
||||
if (endTag && endTag.t === 'Tag' && endTag.isEnd) {
|
||||
var closeTag = endTag.n;
|
||||
var isVoidElement = HTML.isVoidElement(closeTag);
|
||||
scanner.fatal("Unexpected HTML close tag" +
|
||||
(isVoidElement ?
|
||||
'. <' + endTag.n + '> should have no close tag.' : ''));
|
||||
}
|
||||
|
||||
scanner.pos = posBefore; // rewind, we'll continue parsing as usual
|
||||
|
||||
if (! shouldStop)
|
||||
scanner.fatal("Expected EOF");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Take a numeric Unicode code point, which may be larger than 16 bits,
|
||||
// and encode it as a JavaScript UTF-16 string.
|
||||
//
|
||||
// Adapted from
|
||||
// http://stackoverflow.com/questions/7126384/expressing-utf-16-unicode-characters-in-javascript/7126661.
|
||||
codePointToString = HTMLTools.codePointToString = function(cp) {
|
||||
if (cp >= 0 && cp <= 0xD7FF || cp >= 0xE000 && cp <= 0xFFFF) {
|
||||
return String.fromCharCode(cp);
|
||||
} else if (cp >= 0x10000 && cp <= 0x10FFFF) {
|
||||
|
||||
// we substract 0x10000 from cp to get a 20-bit number
|
||||
// in the range 0..0xFFFF
|
||||
cp -= 0x10000;
|
||||
|
||||
// we add 0xD800 to the number formed by the first 10 bits
|
||||
// to give the first byte
|
||||
var first = ((0xffc00 & cp) >> 10) + 0xD800;
|
||||
|
||||
// we add 0xDC00 to the number formed by the low 10 bits
|
||||
// to give the second byte
|
||||
var second = (0x3ff & cp) + 0xDC00;
|
||||
|
||||
return String.fromCharCode(first) + String.fromCharCode(second);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
getContent = HTMLTools.Parse.getContent = function (scanner, shouldStopFunc) {
|
||||
var items = [];
|
||||
|
||||
while (! scanner.isEOF()) {
|
||||
if (shouldStopFunc && shouldStopFunc(scanner))
|
||||
break;
|
||||
|
||||
var posBefore = scanner.pos;
|
||||
var token = getHTMLToken(scanner);
|
||||
if (! token)
|
||||
// tokenizer reached EOF on its own, e.g. while scanning
|
||||
// template comments like `{{! foo}}`.
|
||||
continue;
|
||||
|
||||
if (token.t === 'Doctype') {
|
||||
scanner.fatal("Unexpected Doctype");
|
||||
} else if (token.t === 'Chars') {
|
||||
pushOrAppendString(items, token.v);
|
||||
} else if (token.t === 'CharRef') {
|
||||
items.push(convertCharRef(token));
|
||||
} else if (token.t === 'Comment') {
|
||||
items.push(HTML.Comment(token.v));
|
||||
} else if (token.t === 'Special') {
|
||||
// token.v is an object `{ ... }`
|
||||
items.push(HTMLTools.Special(token.v));
|
||||
} else if (token.t === 'Tag') {
|
||||
if (token.isEnd) {
|
||||
// rewind; we'll parse the end token later
|
||||
scanner.pos = posBefore;
|
||||
break;
|
||||
}
|
||||
|
||||
var tagName = token.n;
|
||||
// is this an element with no close tag (a BR, HR, IMG, etc.) based
|
||||
// on its name?
|
||||
var isVoid = HTML.isVoidElement(tagName);
|
||||
if (token.isSelfClosing) {
|
||||
if (! (isVoid || HTML.isKnownSVGElement(tagName) || tagName.indexOf(':') >= 0))
|
||||
scanner.fatal('Only certain elements like BR, HR, IMG, etc. (and foreign elements like SVG) are allowed to self-close');
|
||||
}
|
||||
|
||||
// may be null
|
||||
var attrs = parseAttrs(token.attrs);
|
||||
|
||||
var tagFunc = HTML.getTag(tagName);
|
||||
if (isVoid || token.isSelfClosing) {
|
||||
items.push(attrs ? tagFunc(attrs) : tagFunc());
|
||||
} else {
|
||||
// parse HTML tag contents.
|
||||
|
||||
// HTML treats a final `/` in a tag as part of an attribute, as in `<a href=/foo/>`, but the template author who writes `<circle r={{r}}/>`, say, may not be thinking about that, so generate a good error message in the "looks like self-close" case.
|
||||
var looksLikeSelfClose = (scanner.input.substr(scanner.pos - 2, 2) === '/>');
|
||||
|
||||
var content;
|
||||
if (token.n === 'textarea') {
|
||||
if (scanner.peek() === '\n')
|
||||
scanner.pos++;
|
||||
content = getRCData(scanner, token.n, shouldStopFunc);
|
||||
} else {
|
||||
content = getContent(scanner, shouldStopFunc);
|
||||
}
|
||||
|
||||
var endTag = getHTMLToken(scanner);
|
||||
|
||||
if (! (endTag && endTag.t === 'Tag' && endTag.isEnd && endTag.n === tagName))
|
||||
scanner.fatal('Expected "' + tagName + '" end tag' + (looksLikeSelfClose ? ' -- if the "<' + token.n + ' />" tag was supposed to self-close, try adding a space before the "/"' : ''));
|
||||
|
||||
// XXX support implied end tags in cases allowed by the spec
|
||||
|
||||
// make `content` into an array suitable for applying tag constructor
|
||||
// as in `FOO.apply(null, content)`.
|
||||
if (content == null)
|
||||
content = [];
|
||||
else if (! (content instanceof Array))
|
||||
content = [content];
|
||||
|
||||
items.push(HTML.getTag(tagName).apply(
|
||||
null, (attrs ? [attrs] : []).concat(content)));
|
||||
}
|
||||
} else {
|
||||
scanner.fatal("Unknown token type: " + token.t);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0)
|
||||
return null;
|
||||
else if (items.length === 1)
|
||||
return items[0];
|
||||
else
|
||||
return items;
|
||||
};
|
||||
|
||||
var pushOrAppendString = function (items, string) {
|
||||
if (items.length &&
|
||||
typeof items[items.length - 1] === 'string')
|
||||
items[items.length - 1] += string;
|
||||
else
|
||||
items.push(string);
|
||||
};
|
||||
|
||||
// get RCDATA to go in the lowercase (or camel case) tagName (e.g. "textarea")
|
||||
getRCData = HTMLTools.Parse.getRCData = function (scanner, tagName, shouldStopFunc) {
|
||||
var items = [];
|
||||
|
||||
while (! scanner.isEOF()) {
|
||||
// break at appropriate end tag
|
||||
if (tagName && isLookingAtEndTag(scanner, tagName))
|
||||
break;
|
||||
|
||||
if (shouldStopFunc && shouldStopFunc(scanner))
|
||||
break;
|
||||
|
||||
var token = getHTMLToken(scanner, 'rcdata');
|
||||
if (! token)
|
||||
// tokenizer reached EOF on its own, e.g. while scanning
|
||||
// template comments like `{{! foo}}`.
|
||||
continue;
|
||||
|
||||
if (token.t === 'Chars') {
|
||||
pushOrAppendString(items, token.v);
|
||||
} else if (token.t === 'CharRef') {
|
||||
items.push(convertCharRef(token));
|
||||
} else if (token.t === 'Special') {
|
||||
// token.v is an object `{ ... }`
|
||||
items.push(HTMLTools.Special(token.v));
|
||||
} else {
|
||||
// (can't happen)
|
||||
scanner.fatal("Unknown or unexpected token type: " + token.t);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0)
|
||||
return null;
|
||||
else if (items.length === 1)
|
||||
return items[0];
|
||||
else
|
||||
return items;
|
||||
};
|
||||
|
||||
var getRawText = function (scanner, tagName, shouldStopFunc) {
|
||||
var items = [];
|
||||
|
||||
while (! scanner.isEOF()) {
|
||||
// break at appropriate end tag
|
||||
if (tagName && isLookingAtEndTag(scanner, tagName))
|
||||
break;
|
||||
|
||||
if (shouldStopFunc && shouldStopFunc(scanner))
|
||||
break;
|
||||
|
||||
var token = getHTMLToken(scanner, 'rawtext');
|
||||
if (! token)
|
||||
// tokenizer reached EOF on its own, e.g. while scanning
|
||||
// template comments like `{{! foo}}`.
|
||||
continue;
|
||||
|
||||
if (token.t === 'Chars') {
|
||||
pushOrAppendString(items, token.v);
|
||||
} else if (token.t === 'Special') {
|
||||
// token.v is an object `{ ... }`
|
||||
items.push(HTMLTools.Special(token.v));
|
||||
} else {
|
||||
// (can't happen)
|
||||
scanner.fatal("Unknown or unexpected token type: " + token.t);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0)
|
||||
return null;
|
||||
else if (items.length === 1)
|
||||
return items[0];
|
||||
else
|
||||
return items;
|
||||
};
|
||||
|
||||
// Input: A token like `{ t: 'CharRef', v: '&', cp: [38] }`.
|
||||
//
|
||||
// Output: A tag like `HTML.CharRef({ html: '&', str: '&' })`.
|
||||
var convertCharRef = function (token) {
|
||||
var codePoints = token.cp;
|
||||
var str = '';
|
||||
for (var i = 0; i < codePoints.length; i++)
|
||||
str += codePointToString(codePoints[i]);
|
||||
return HTML.CharRef({ html: token.v, str: str });
|
||||
};
|
||||
|
||||
// Input is always a dictionary (even if zero attributes) and each
|
||||
// value in the dictionary is an array of `Chars`, `CharRef`,
|
||||
// and maybe `Special` tokens.
|
||||
//
|
||||
// Output is null if there are zero attributes, and otherwise a
|
||||
// dictionary. Each value in the dictionary is HTMLjs (e.g. a
|
||||
// string or an array of `Chars`, `CharRef`, and `Special`
|
||||
// nodes).
|
||||
//
|
||||
// An attribute value with no input tokens is represented as "",
|
||||
// not an empty array, in order to prop open empty attributes
|
||||
// with no template tags.
|
||||
var parseAttrs = function (attrs) {
|
||||
var result = null;
|
||||
|
||||
for (var k in attrs) {
|
||||
if (! result)
|
||||
result = {};
|
||||
|
||||
var inValue = attrs[k];
|
||||
var outParts = [];
|
||||
for (var i = 0; i < inValue.length; i++) {
|
||||
var token = inValue[i];
|
||||
if (token.t === 'CharRef') {
|
||||
outParts.push(convertCharRef(token));
|
||||
} else if (token.t === 'Special') {
|
||||
outParts.push(HTMLTools.Special(token.v));
|
||||
} else if (token.t === 'Chars') {
|
||||
pushOrAppendString(outParts, token.v);
|
||||
}
|
||||
}
|
||||
|
||||
if (k === '$specials') {
|
||||
// the `$specials` pseudo-attribute should always get an
|
||||
// array, even if there is only one Special.
|
||||
result[k] = outParts;
|
||||
} else {
|
||||
var outValue = (inValue.length === 0 ? '' :
|
||||
(outParts.length === 1 ? outParts[0] : outParts));
|
||||
var properKey = HTMLTools.properCaseAttributeName(k);
|
||||
result[properKey] = outValue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
360
packages/html-tools/parse_tests.js
Normal file
360
packages/html-tools/parse_tests.js
Normal file
@@ -0,0 +1,360 @@
|
||||
var Scanner = HTMLTools.Scanner;
|
||||
var getContent = HTMLTools.Parse.getContent;
|
||||
|
||||
var CharRef = HTML.CharRef;
|
||||
var Comment = HTML.Comment;
|
||||
var Special = HTMLTools.Special;
|
||||
|
||||
var BR = HTML.BR;
|
||||
var HR = HTML.HR;
|
||||
var INPUT = HTML.INPUT;
|
||||
var A = HTML.A;
|
||||
var DIV = HTML.DIV;
|
||||
var P = HTML.P;
|
||||
var TEXTAREA = HTML.TEXTAREA;
|
||||
|
||||
Tinytest.add("html-tools - parser getContent", function (test) {
|
||||
|
||||
var succeed = function (input, expected) {
|
||||
var endPos = input.indexOf('^^^');
|
||||
if (endPos < 0)
|
||||
endPos = input.length;
|
||||
|
||||
var scanner = new Scanner(input.replace('^^^', ''));
|
||||
var result = getContent(scanner);
|
||||
test.equal(scanner.pos, endPos);
|
||||
test.equal(HTML.toJS(result), HTML.toJS(expected));
|
||||
};
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
var scanner = new Scanner(input);
|
||||
var error;
|
||||
try {
|
||||
getContent(scanner);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (messageContains)
|
||||
test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
|
||||
succeed('', null);
|
||||
succeed('abc', 'abc');
|
||||
succeed('abc^^^</x>', 'abc');
|
||||
succeed('a<b', ['a', CharRef({html: '<', str: '<'}), 'b']);
|
||||
succeed('<!-- x -->', Comment(' x '));
|
||||
succeed('∾̳', CharRef({html: '∾̳', str: '\u223e\u0333'}));
|
||||
succeed('𝕫', CharRef({html: '𝕫', str: '\ud835\udd6b'}));
|
||||
succeed('&&>&g>;', ['&&>&g', CharRef({html: '>', str: '>'}), ';']);
|
||||
|
||||
// Can't have an unescaped `&` if followed by certain names like `gt`
|
||||
fatal('>&');
|
||||
// tests for other failure cases
|
||||
fatal('<');
|
||||
|
||||
succeed('<br>', BR());
|
||||
succeed('<br/>', BR());
|
||||
fatal('<div/>', 'self-close');
|
||||
|
||||
succeed('<hr id=foo>', HR({id:'foo'}));
|
||||
succeed('<hr id=<foo>>', HR({id:[CharRef({html:'<', str:'<'}),
|
||||
'foo',
|
||||
CharRef({html:'>', str:'>'})]}));
|
||||
succeed('<input selected>', INPUT({selected: ''}));
|
||||
succeed('<input selected/>', INPUT({selected: ''}));
|
||||
succeed('<input selected />', INPUT({selected: ''}));
|
||||
var FOO = HTML.getTag('foo');
|
||||
succeed('<foo bar></foo>', FOO({bar: ''}));
|
||||
succeed('<foo bar baz ></foo>', FOO({bar: '', baz: ''}));
|
||||
succeed('<foo bar=x baz qux=y blah ></foo>',
|
||||
FOO({bar: 'x', baz: '', qux: 'y', blah: ''}));
|
||||
succeed('<foo bar="x" baz qux="y" blah ></foo>',
|
||||
FOO({bar: 'x', baz: '', qux: 'y', blah: ''}));
|
||||
fatal('<input bar"baz">');
|
||||
fatal('<input x="y"z >');
|
||||
fatal('<input x=\'y\'z >');
|
||||
succeed('<br x=&&&>', BR({x: '&&&'}));
|
||||
succeed('<br><br><br>', [BR(), BR(), BR()]);
|
||||
succeed('aaa<br>\nbbb<br>\nccc<br>', ['aaa', BR(), '\nbbb', BR(), '\nccc', BR()]);
|
||||
|
||||
succeed('<a></a>', A());
|
||||
fatal('<');
|
||||
fatal('<a');
|
||||
fatal('<a>');
|
||||
fatal('<a><');
|
||||
fatal('<a></');
|
||||
fatal('<a></a');
|
||||
|
||||
succeed('<a href="http://www.apple.com/">Apple</a>',
|
||||
A({href: "http://www.apple.com/"}, 'Apple'));
|
||||
|
||||
(function () {
|
||||
var A = HTML.getTag('a');
|
||||
var B = HTML.getTag('b');
|
||||
var C = HTML.getTag('c');
|
||||
var D = HTML.getTag('d');
|
||||
|
||||
succeed('<a>1<b>2<c>3<d>4</d>5</c>6</b>7</a>8',
|
||||
[A('1', B('2', C('3', D('4'), '5'), '6'), '7'), '8']);
|
||||
})();
|
||||
|
||||
fatal('<b>hello <i>there</b> world</i>');
|
||||
|
||||
// XXX support implied end tags in cases allowed by the spec
|
||||
fatal('<p>');
|
||||
|
||||
fatal('<a>Foo</a/>');
|
||||
fatal('<a>Foo</a b=c>');
|
||||
|
||||
succeed('<textarea>asdf</textarea>', TEXTAREA("asdf"));
|
||||
succeed('<textarea x=y>asdf</textarea>', TEXTAREA({x: "y"}, "asdf"));
|
||||
succeed('<textarea><p></textarea>', TEXTAREA("<p>"));
|
||||
succeed('<textarea>a&b</textarea>',
|
||||
TEXTAREA("a", CharRef({html: '&', str: '&'}), "b"));
|
||||
succeed('<textarea></textarea</textarea>', TEXTAREA("</textarea"));
|
||||
// absorb up to one initial newline, as per HTML parsing spec
|
||||
succeed('<textarea>\n</textarea>', TEXTAREA());
|
||||
succeed('<textarea>\nasdf</textarea>', TEXTAREA("asdf"));
|
||||
succeed('<textarea>\n\nasdf</textarea>', TEXTAREA("\nasdf"));
|
||||
succeed('<textarea>\n\n</textarea>', TEXTAREA("\n"));
|
||||
succeed('<textarea>\nasdf\n</textarea>', TEXTAREA("asdf\n"));
|
||||
succeed('<textarea><!-- --></textarea>', TEXTAREA("<!-- -->"));
|
||||
succeed('<tExTaReA>asdf</TEXTarea>', TEXTAREA("asdf"));
|
||||
fatal('<textarea>asdf');
|
||||
fatal('<textarea>asdf</textarea');
|
||||
fatal('<textarea>&davidgreenspan;</textarea>');
|
||||
succeed('<textarea>&</textarea>', TEXTAREA("&"));
|
||||
succeed('<textarea></textarea \n<</textarea \n>asdf',
|
||||
[TEXTAREA("</textarea \n<"), "asdf"]);
|
||||
|
||||
// CR/LF behavior
|
||||
succeed('<br\r\n x>', BR({x:''}));
|
||||
succeed('<br\r x>', BR({x:''}));
|
||||
succeed('<br x="y"\r\n>', BR({x:'y'}));
|
||||
succeed('<br x="y"\r>', BR({x:'y'}));
|
||||
succeed('<br x=\r\n"y">', BR({x:'y'}));
|
||||
succeed('<br x=\r"y">', BR({x:'y'}));
|
||||
succeed('<br x\r=\r"y">', BR({x:'y'}));
|
||||
succeed('<!--\r\n-->', Comment('\n'));
|
||||
succeed('<!--\r-->', Comment('\n'));
|
||||
succeed('<textarea>a\r\nb\r\nc</textarea>', TEXTAREA('a\nb\nc'));
|
||||
succeed('<textarea>a\rb\rc</textarea>', TEXTAREA('a\nb\nc'));
|
||||
succeed('<br x="\r\n\r\n">', BR({x:'\n\n'}));
|
||||
succeed('<br x="\r\r">', BR({x:'\n\n'}));
|
||||
succeed('<br x=y\r>', BR({x:'y'}));
|
||||
fatal('<br x=\r>');
|
||||
});
|
||||
|
||||
Tinytest.add("html-tools - parseFragment", function (test) {
|
||||
test.equal(HTML.toJS(HTMLTools.parseFragment("<div><p id=foo>Hello</p></div>")),
|
||||
HTML.toJS(DIV(P({id:'foo'}, 'Hello'))));
|
||||
|
||||
_.each(['asdf</br>', '{{!foo}}</br>', '{{!foo}} </br>',
|
||||
'asdf</a>', '{{!foo}}</a>', '{{!foo}} </a>'], function (badFrag) {
|
||||
test.throws(function() {
|
||||
HTMLTools.parseFragment(badFrag);
|
||||
}, /Unexpected HTML close tag/);
|
||||
});
|
||||
|
||||
(function () {
|
||||
var p = HTMLTools.parseFragment('<p></p>');
|
||||
test.equal(p.tagName, 'p');
|
||||
test.equal(p.attrs, null);
|
||||
test.isTrue(p instanceof HTML.Tag);
|
||||
test.equal(p.children.length, 0);
|
||||
})();
|
||||
|
||||
(function () {
|
||||
var p = HTMLTools.parseFragment('<p>x</p>');
|
||||
test.equal(p.tagName, 'p');
|
||||
test.equal(p.attrs, null);
|
||||
test.isTrue(p instanceof HTML.Tag);
|
||||
test.equal(p.children.length, 1);
|
||||
test.equal(p.children[0], 'x');
|
||||
})();
|
||||
|
||||
(function () {
|
||||
var p = HTMLTools.parseFragment('<p>xA</p>');
|
||||
test.equal(p.tagName, 'p');
|
||||
test.equal(p.attrs, null);
|
||||
test.isTrue(p instanceof HTML.Tag);
|
||||
test.equal(p.children.length, 2);
|
||||
test.equal(p.children[0], 'x');
|
||||
|
||||
test.isTrue(p.children[1] instanceof HTML.CharRef);
|
||||
test.equal(p.children[1].html, 'A');
|
||||
test.equal(p.children[1].str, 'A');
|
||||
})();
|
||||
|
||||
(function () {
|
||||
var pp = HTMLTools.parseFragment('<p>x</p><p>y</p>');
|
||||
test.isTrue(pp instanceof Array);
|
||||
test.equal(pp.length, 2);
|
||||
|
||||
test.equal(pp[0].tagName, 'p');
|
||||
test.equal(pp[0].attrs, null);
|
||||
test.isTrue(pp[0] instanceof HTML.Tag);
|
||||
test.equal(pp[0].children.length, 1);
|
||||
test.equal(pp[0].children[0], 'x');
|
||||
|
||||
test.equal(pp[1].tagName, 'p');
|
||||
test.equal(pp[1].attrs, null);
|
||||
test.isTrue(pp[1] instanceof HTML.Tag);
|
||||
test.equal(pp[1].children.length, 1);
|
||||
test.equal(pp[1].children[0], 'y');
|
||||
})();
|
||||
|
||||
var scanner = new Scanner('asdf');
|
||||
scanner.pos = 1;
|
||||
test.equal(HTMLTools.parseFragment(scanner), 'sdf');
|
||||
|
||||
test.throws(function () {
|
||||
var scanner = new Scanner('asdf</p>');
|
||||
scanner.pos = 1;
|
||||
HTMLTools.parseFragment(scanner);
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("html-tools - getSpecialTag", function (test) {
|
||||
|
||||
// match a simple tag consisting of `{{`, an optional `!`, one
|
||||
// or more ASCII letters, spaces or html tags, and a closing `}}`.
|
||||
var mustache = /^\{\{(!?[a-zA-Z 0-9</>]+)\}\}/;
|
||||
|
||||
// This implementation of `getSpecialTag` looks for "{{" and if it
|
||||
// finds it, it will match the regex above or fail fatally trying.
|
||||
// The object it returns is opaque to the tokenizer/parser and can
|
||||
// be anything we want.
|
||||
var getSpecialTag = function (scanner, position) {
|
||||
if (! (scanner.peek() === '{' && // one-char peek is just an optimization
|
||||
scanner.rest().slice(0, 2) === '{{'))
|
||||
return null;
|
||||
|
||||
var match = mustache.exec(scanner.rest());
|
||||
if (! match)
|
||||
scanner.fatal("Bad mustache");
|
||||
|
||||
scanner.pos += match[0].length;
|
||||
|
||||
if (match[1].charAt(0) === '!')
|
||||
return null; // `{{!foo}}` is like a comment
|
||||
|
||||
return { stuff: match[1] };
|
||||
};
|
||||
|
||||
|
||||
|
||||
var succeed = function (input, expected) {
|
||||
var endPos = input.indexOf('^^^');
|
||||
if (endPos < 0)
|
||||
endPos = input.length;
|
||||
|
||||
var scanner = new Scanner(input.replace('^^^', ''));
|
||||
scanner.getSpecialTag = getSpecialTag;
|
||||
var result;
|
||||
try {
|
||||
result = getContent(scanner);
|
||||
} catch (e) {
|
||||
result = String(e);
|
||||
}
|
||||
test.equal(scanner.pos, endPos);
|
||||
test.equal(HTML.toJS(result), HTML.toJS(expected));
|
||||
};
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
var scanner = new Scanner(input);
|
||||
scanner.getSpecialTag = getSpecialTag;
|
||||
var error;
|
||||
try {
|
||||
getContent(scanner);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (messageContains)
|
||||
test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
|
||||
succeed('{{foo}}', Special({stuff: 'foo'}));
|
||||
|
||||
succeed('<a href=http://www.apple.com/>{{foo}}</a>',
|
||||
A({href: "http://www.apple.com/"}, Special({stuff: 'foo'})));
|
||||
|
||||
// tags not parsed in comments
|
||||
succeed('<!--{{foo}}-->', Comment("{{foo}}"));
|
||||
succeed('<!--{{foo-->', Comment("{{foo"));
|
||||
|
||||
succeed('&am{{foo}}p;', ['&am', Special({stuff: 'foo'}), 'p;']);
|
||||
|
||||
// can't start a mustache and not finish it
|
||||
fatal('{{foo');
|
||||
fatal('<a>{{</a>');
|
||||
|
||||
// no mustache allowed in tag name
|
||||
fatal('<{{a}}>');
|
||||
fatal('<{{a}}b>');
|
||||
fatal('<a{{b}}>');
|
||||
|
||||
// single curly brace is no biggie
|
||||
succeed('a{b', 'a{b');
|
||||
succeed('<br x={ />', BR({x:'{'}));
|
||||
succeed('<br x={foo} />', BR({x:'{foo}'}));
|
||||
|
||||
succeed('<br {{x}}>', BR({$specials: [Special({stuff: 'x'})]}));
|
||||
succeed('<br {{x}} {{y}}>', BR({$specials: [Special({stuff: 'x'}),
|
||||
Special({stuff: 'y'})]}));
|
||||
succeed('<br {{x}} y>', BR({$specials: [Special({stuff: 'x'})], y:''}));
|
||||
fatal('<br {{x}}y>');
|
||||
fatal('<br {{x}}=y>');
|
||||
succeed('<br x={{y}} z>', BR({x: Special({stuff: 'y'}), z: ''}));
|
||||
succeed('<br x=y{{z}}w>', BR({x: ['y', Special({stuff: 'z'}), 'w']}));
|
||||
succeed('<br x="y{{z}}w">', BR({x: ['y', Special({stuff: 'z'}), 'w']}));
|
||||
succeed('<br x="y {{z}}{{w}} v">', BR({x: ['y ', Special({stuff: 'z'}),
|
||||
Special({stuff: 'w'}), ' v']}));
|
||||
// Slash is parsed as part of unquoted attribute! This is consistent with
|
||||
// the HTML tokenization spec. It seems odd for some inputs but is probably
|
||||
// for cases like `<a href=http://foo.com/>` or `<a href=/foo/>`.
|
||||
succeed('<br x={{y}}/>', BR({x: [Special({stuff: 'y'}), '/']}));
|
||||
succeed('<br x={{z}}{{w}}>', BR({x: [Special({stuff: 'z'}),
|
||||
Special({stuff: 'w'})]}));
|
||||
fatal('<br x="y"{{z}}>');
|
||||
|
||||
succeed('<br x=&>', BR({x:CharRef({html: '&', str: '&'})}));
|
||||
|
||||
|
||||
// check tokenization of stache tags with spaces
|
||||
succeed('<br {{x 1}}>', BR({$specials: [Special({stuff: 'x 1'})]}));
|
||||
succeed('<br {{x 1}} {{y 2}}>', BR({$specials: [Special({stuff: 'x 1'}),
|
||||
Special({stuff: 'y 2'})]}));
|
||||
succeed('<br {{x 1}} y>', BR({$specials: [Special({stuff: 'x 1'})], y:''}));
|
||||
fatal('<br {{x 1}}y>');
|
||||
fatal('<br {{x 1}}=y>');
|
||||
succeed('<br x={{y 2}} z>', BR({x: Special({stuff: 'y 2'}), z: ''}));
|
||||
succeed('<br x=y{{z 3}}w>', BR({x: ['y', Special({stuff: 'z 3'}), 'w']}));
|
||||
succeed('<br x="y{{z 3}}w">', BR({x: ['y', Special({stuff: 'z 3'}), 'w']}));
|
||||
succeed('<br x="y {{z 3}}{{w 4}} v">', BR({x: ['y ', Special({stuff: 'z 3'}),
|
||||
Special({stuff: 'w 4'}), ' v']}));
|
||||
succeed('<br x={{y 2}}/>', BR({x: [Special({stuff: 'y 2'}), '/']}));
|
||||
succeed('<br x={{z 3}}{{w 4}}>', BR({x: [Special({stuff: 'z 3'}),
|
||||
Special({stuff: 'w 4'})]}));
|
||||
|
||||
succeed('<p></p>', P());
|
||||
|
||||
succeed('x{{foo}}{{bar}}y', ['x', Special({stuff: 'foo'}),
|
||||
Special({stuff: 'bar'}), 'y']);
|
||||
succeed('x{{!foo}}{{!bar}}y', 'xy');
|
||||
succeed('x{{!foo}}{{bar}}y', ['x', Special({stuff: 'bar'}), 'y']);
|
||||
succeed('x{{foo}}{{!bar}}y', ['x', Special({stuff: 'foo'}), 'y']);
|
||||
succeed('<div>{{!foo}}{{!bar}}</div>', DIV());
|
||||
succeed('<div>{{!foo}}<br />{{!bar}}</div>', DIV(BR()));
|
||||
succeed('<div> {{!foo}} {{!bar}} </div>', DIV(" "));
|
||||
succeed('<div> {{!foo}} <br /> {{!bar}}</div>', DIV(" ", BR(), " "));
|
||||
succeed('{{! <div></div> }}', null);
|
||||
succeed('{{!<div></div>}}', null);
|
||||
|
||||
succeed('', null);
|
||||
succeed('{{!foo}}', null);
|
||||
});
|
||||
82
packages/html-tools/scanner.js
Normal file
82
packages/html-tools/scanner.js
Normal file
@@ -0,0 +1,82 @@
|
||||
// This is a Scanner class suitable for any parser/lexer/tokenizer.
|
||||
//
|
||||
// A Scanner has an immutable source document (string) `input` and a current
|
||||
// position `pos`, an index into the string, which can be set at will.
|
||||
//
|
||||
// * `new Scanner(input)` - constructs a Scanner with source string `input`
|
||||
// * `scanner.rest()` - returns the rest of the input after `pos`
|
||||
// * `scanner.peek()` - returns the character at `pos`
|
||||
// * `scanner.isEOF()` - true if `pos` is at or beyond the end of `input`
|
||||
// * `scanner.fatal(msg)` - throw an error indicating a problem at `pos`
|
||||
|
||||
Scanner = HTMLTools.Scanner = function (input) {
|
||||
this.input = input; // public, read-only
|
||||
this.pos = 0; // public, read-write
|
||||
};
|
||||
|
||||
Scanner.prototype.rest = function () {
|
||||
// Slicing a string is O(1) in modern JavaScript VMs (including old IE).
|
||||
return this.input.slice(this.pos);
|
||||
};
|
||||
|
||||
Scanner.prototype.isEOF = function () {
|
||||
return this.pos >= this.input.length;
|
||||
};
|
||||
|
||||
Scanner.prototype.fatal = function (msg) {
|
||||
// despite this default, you should always provide a message!
|
||||
msg = (msg || "Parse error");
|
||||
|
||||
var CONTEXT_AMOUNT = 20;
|
||||
|
||||
var input = this.input;
|
||||
var pos = this.pos;
|
||||
var pastInput = input.substring(pos - CONTEXT_AMOUNT - 1, pos);
|
||||
if (pastInput.length > CONTEXT_AMOUNT)
|
||||
pastInput = '...' + pastInput.substring(-CONTEXT_AMOUNT);
|
||||
|
||||
var upcomingInput = input.substring(pos, pos + CONTEXT_AMOUNT + 1);
|
||||
if (upcomingInput.length > CONTEXT_AMOUNT)
|
||||
upcomingInput = upcomingInput.substring(0, CONTEXT_AMOUNT) + '...';
|
||||
|
||||
var positionDisplay = ((pastInput + upcomingInput).replace(/\n/g, ' ') + '\n' +
|
||||
(new Array(pastInput.length + 1).join(' ')) + "^");
|
||||
|
||||
var e = new Error(msg + "\n" + positionDisplay);
|
||||
|
||||
e.offset = pos;
|
||||
var allPastInput = input.substring(0, pos);
|
||||
e.line = (1 + (allPastInput.match(/\n/g) || []).length);
|
||||
e.col = (1 + pos - allPastInput.lastIndexOf('\n'));
|
||||
e.scanner = this;
|
||||
|
||||
throw e;
|
||||
};
|
||||
|
||||
// Peek at the next character.
|
||||
//
|
||||
// If `isEOF`, returns an empty string.
|
||||
Scanner.prototype.peek = function () {
|
||||
return this.input.charAt(this.pos);
|
||||
};
|
||||
|
||||
// Constructs a `getFoo` function where `foo` is specified with a regex.
|
||||
// The regex should start with `^`. The constructed function will return
|
||||
// match group 1, if it exists and matches a non-empty string, or else
|
||||
// the entire matched string (or null if there is no match).
|
||||
//
|
||||
// A `getFoo` function tries to match and consume a foo. If it succeeds,
|
||||
// the current position of the scanner is advanced. If it fails, the
|
||||
// current position is not advanced and a falsy value (typically null)
|
||||
// is returned.
|
||||
makeRegexMatcher = function (regex) {
|
||||
return function (scanner) {
|
||||
var match = regex.exec(scanner.rest());
|
||||
|
||||
if (! match)
|
||||
return null;
|
||||
|
||||
scanner.pos += match[0].length;
|
||||
return match[1] || match[0];
|
||||
};
|
||||
};
|
||||
532
packages/html-tools/tokenize.js
Normal file
532
packages/html-tools/tokenize.js
Normal file
@@ -0,0 +1,532 @@
|
||||
// Token types:
|
||||
//
|
||||
// { t: 'Doctype',
|
||||
// v: String (entire Doctype declaration from the source),
|
||||
// name: String,
|
||||
// systemId: String (optional),
|
||||
// publicId: String (optional)
|
||||
// }
|
||||
//
|
||||
// { t: 'Comment',
|
||||
// v: String (not including "<!--" and "-->")
|
||||
// }
|
||||
//
|
||||
// { t: 'Chars',
|
||||
// v: String (pure text like you might pass to document.createTextNode,
|
||||
// no character references)
|
||||
// }
|
||||
//
|
||||
// { t: 'Tag',
|
||||
// isEnd: Boolean (optional),
|
||||
// isSelfClosing: Boolean (optional),
|
||||
// n: String (tag name, in lowercase or camel case),
|
||||
// attrs: { String: [zero or more 'Chars' or 'CharRef' objects] }
|
||||
// (only for start tags; required)
|
||||
// }
|
||||
//
|
||||
// { t: 'CharRef',
|
||||
// v: String (entire character reference from the source, e.g. "&"),
|
||||
// cp: [Integer] (array of Unicode code point numbers it expands to)
|
||||
// }
|
||||
//
|
||||
// We keep around both the original form of the character reference and its
|
||||
// expansion so that subsequent processing steps have the option to
|
||||
// re-emit it (if they are generating HTML) or interpret it. Named and
|
||||
// numerical code points may be more than 16 bits, in which case they
|
||||
// need to passed through codePointToString to make a JavaScript string.
|
||||
// Most named entities and all numeric character references are one codepoint
|
||||
// (e.g. "&" is [38]), but a few are two codepoints.
|
||||
//
|
||||
// { t: 'Special',
|
||||
// v: { ... anything ... }
|
||||
// }
|
||||
|
||||
// The HTML tokenization spec says to preprocess the input stream to replace
|
||||
// CR(LF)? with LF. However, preprocessing `scanner` would complicate things
|
||||
// by making indexes not match the input (e.g. for error messages), so we just
|
||||
// keep in mind as we go along that an LF might be represented by CRLF or CR.
|
||||
// In most cases, it doesn't actually matter what combination of whitespace
|
||||
// characters are present (e.g. inside tags).
|
||||
var HTML_SPACE = /^[\f\n\r\t ]/;
|
||||
|
||||
var convertCRLF = function (str) {
|
||||
return str.replace(/\r\n?/g, '\n');
|
||||
};
|
||||
|
||||
getComment = HTMLTools.Parse.getComment = function (scanner) {
|
||||
if (scanner.rest().slice(0, 4) !== '<!--')
|
||||
return null;
|
||||
scanner.pos += 4;
|
||||
|
||||
// Valid comments are easy to parse; they end at the first `--`!
|
||||
// Our main job is throwing errors.
|
||||
|
||||
var rest = scanner.rest();
|
||||
if (rest.charAt(0) === '>' || rest.slice(0, 2) === '->')
|
||||
scanner.fatal("HTML comment can't start with > or ->");
|
||||
|
||||
var closePos = rest.indexOf('-->');
|
||||
if (closePos < 0)
|
||||
scanner.fatal("Unclosed HTML comment");
|
||||
|
||||
var commentContents = rest.slice(0, closePos);
|
||||
if (commentContents.slice(-1) === '-')
|
||||
scanner.fatal("HTML comment must end at first `--`");
|
||||
if (commentContents.indexOf("--") >= 0)
|
||||
scanner.fatal("HTML comment cannot contain `--` anywhere");
|
||||
if (commentContents.indexOf('\u0000') >= 0)
|
||||
scanner.fatal("HTML comment cannot contain NULL");
|
||||
|
||||
scanner.pos += closePos + 3;
|
||||
|
||||
return { t: 'Comment',
|
||||
v: convertCRLF(commentContents) };
|
||||
};
|
||||
|
||||
var skipSpaces = function (scanner) {
|
||||
while (HTML_SPACE.test(scanner.peek()))
|
||||
scanner.pos++;
|
||||
};
|
||||
|
||||
var requireSpaces = function (scanner) {
|
||||
if (! HTML_SPACE.test(scanner.peek()))
|
||||
scanner.fatal("Expected space");
|
||||
skipSpaces(scanner);
|
||||
};
|
||||
|
||||
var getDoctypeQuotedString = function (scanner) {
|
||||
var quote = scanner.peek();
|
||||
if (! (quote === '"' || quote === "'"))
|
||||
scanner.fatal("Expected single or double quote in DOCTYPE");
|
||||
scanner.pos++;
|
||||
|
||||
if (scanner.peek() === quote)
|
||||
// prevent a falsy return value (empty string)
|
||||
scanner.fatal("Malformed DOCTYPE");
|
||||
|
||||
var str = '';
|
||||
var ch;
|
||||
while ((ch = scanner.peek()), ch !== quote) {
|
||||
if ((! ch) || (ch === '\u0000') || (ch === '>'))
|
||||
scanner.fatal("Malformed DOCTYPE");
|
||||
str += ch;
|
||||
scanner.pos++;
|
||||
}
|
||||
|
||||
scanner.pos++;
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
// See http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#the-doctype.
|
||||
//
|
||||
// If `getDocType` sees "<!DOCTYPE" (case-insensitive), it will match or fail fatally.
|
||||
getDoctype = HTMLTools.Parse.getDoctype = function (scanner) {
|
||||
if (HTMLTools.asciiLowerCase(scanner.rest().slice(0, 9)) !== '<!doctype')
|
||||
return null;
|
||||
var start = scanner.pos;
|
||||
scanner.pos += 9;
|
||||
|
||||
requireSpaces(scanner);
|
||||
|
||||
var ch = scanner.peek();
|
||||
if ((! ch) || (ch === '>') || (ch === '\u0000'))
|
||||
scanner.fatal('Malformed DOCTYPE');
|
||||
var name = ch;
|
||||
scanner.pos++;
|
||||
|
||||
while ((ch = scanner.peek()), ! (HTML_SPACE.test(ch) || ch === '>')) {
|
||||
if ((! ch) || (ch === '\u0000'))
|
||||
scanner.fatal('Malformed DOCTYPE');
|
||||
name += ch;
|
||||
scanner.pos++;
|
||||
}
|
||||
name = HTMLTools.asciiLowerCase(name);
|
||||
|
||||
// Now we're looking at a space or a `>`.
|
||||
skipSpaces(scanner);
|
||||
|
||||
var systemId = null;
|
||||
var publicId = null;
|
||||
|
||||
if (scanner.peek() !== '>') {
|
||||
// Now we're essentially in the "After DOCTYPE name state" of the tokenizer,
|
||||
// but we're not looking at space or `>`.
|
||||
|
||||
// this should be "public" or "system".
|
||||
var publicOrSystem = HTMLTools.asciiLowerCase(scanner.rest().slice(0, 6));
|
||||
|
||||
if (publicOrSystem === 'system') {
|
||||
scanner.pos += 6;
|
||||
requireSpaces(scanner);
|
||||
systemId = getDoctypeQuotedString(scanner);
|
||||
skipSpaces(scanner);
|
||||
if (scanner.peek() !== '>')
|
||||
scanner.fatal("Malformed DOCTYPE");
|
||||
} else if (publicOrSystem === 'public') {
|
||||
scanner.pos += 6;
|
||||
requireSpaces(scanner);
|
||||
publicId = getDoctypeQuotedString(scanner);
|
||||
if (scanner.peek() !== '>') {
|
||||
requireSpaces(scanner);
|
||||
if (scanner.peek() !== '>') {
|
||||
systemId = getDoctypeQuotedString(scanner);
|
||||
skipSpaces(scanner);
|
||||
if (scanner.peek() !== '>')
|
||||
scanner.fatal("Malformed DOCTYPE");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scanner.fatal("Expected PUBLIC or SYSTEM in DOCTYPE");
|
||||
}
|
||||
}
|
||||
|
||||
// looking at `>`
|
||||
scanner.pos++;
|
||||
var result = { t: 'Doctype',
|
||||
v: scanner.input.slice(start, scanner.pos),
|
||||
name: name };
|
||||
|
||||
if (systemId)
|
||||
result.systemId = systemId;
|
||||
if (publicId)
|
||||
result.publicId = publicId;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// The special character `{` is only allowed as the first character
|
||||
// of a Chars, so that we have a chance to detect template tags.
|
||||
var getChars = makeRegexMatcher(/^[^&<\u0000][^&<\u0000{]*/);
|
||||
|
||||
// Returns the next HTML token, or `null` if we reach EOF.
|
||||
//
|
||||
// Note that if we have a `getSpecialTag` function that sometimes
|
||||
// consumes characters and emits nothing (e.g. in the case of template
|
||||
// comments), we may go from not-at-EOF to at-EOF and return `null`,
|
||||
// while otherwise we always find some token to return.
|
||||
getHTMLToken = HTMLTools.Parse.getHTMLToken = function (scanner, dataMode) {
|
||||
var result = null;
|
||||
if (scanner.getSpecialTag) {
|
||||
var lastPos = -1;
|
||||
// Try to parse a "special tag" by calling out to the provided
|
||||
// `getSpecialTag` function. If the function returns `null` but
|
||||
// consumes characters, it must have parsed a comment or something,
|
||||
// so we loop and try it again. If it ever returns `null` without
|
||||
// consuming anything, that means it didn't see anything interesting
|
||||
// so we look for a normal token. If it returns a truthy value,
|
||||
// the value must be an object. We wrap it in a Special token.
|
||||
while ((! result) && scanner.pos > lastPos) {
|
||||
lastPos = scanner.pos;
|
||||
result = scanner.getSpecialTag(
|
||||
scanner,
|
||||
(dataMode === 'rcdata' ? TEMPLATE_TAG_POSITION.IN_RCDATA :
|
||||
(dataMode === 'rawtext' ? TEMPLATE_TAG_POSITION.IN_RAWTEXT :
|
||||
TEMPLATE_TAG_POSITION.ELEMENT)));
|
||||
}
|
||||
if (result)
|
||||
return { t: 'Special', v: result };
|
||||
}
|
||||
|
||||
var chars = getChars(scanner);
|
||||
if (chars)
|
||||
return { t: 'Chars',
|
||||
v: convertCRLF(chars) };
|
||||
|
||||
var ch = scanner.peek();
|
||||
if (! ch)
|
||||
return null; // EOF
|
||||
|
||||
if (ch === '\u0000')
|
||||
scanner.fatal("Illegal NULL character");
|
||||
|
||||
if (ch === '&') {
|
||||
if (dataMode !== 'rawtext') {
|
||||
var charRef = getCharacterReference(scanner);
|
||||
if (charRef)
|
||||
return charRef;
|
||||
}
|
||||
|
||||
scanner.pos++;
|
||||
return { t: 'Chars',
|
||||
v: '&' };
|
||||
}
|
||||
|
||||
// If we're here, we're looking at `<`.
|
||||
|
||||
if (scanner.peek() === '<' && dataMode) {
|
||||
// don't interpret tags
|
||||
scanner.pos++;
|
||||
return { t: 'Chars',
|
||||
v: '<' };
|
||||
}
|
||||
|
||||
// `getTag` will claim anything starting with `<` not followed by `!`.
|
||||
// `getComment` takes `<!--` and getDoctype takes `<!doctype`.
|
||||
result = (getTagToken(scanner) || getComment(scanner) || getDoctype(scanner));
|
||||
|
||||
if (result)
|
||||
return result;
|
||||
|
||||
scanner.fatal("Unexpected `<!` directive.");
|
||||
};
|
||||
|
||||
var getTagName = makeRegexMatcher(/^[a-zA-Z][^\f\n\r\t />{]*/);
|
||||
var getClangle = makeRegexMatcher(/^>/);
|
||||
var getSlash = makeRegexMatcher(/^\//);
|
||||
var getAttributeName = makeRegexMatcher(/^[^>/\u0000"'<=\f\n\r\t ][^\f\n\r\t /=>"'<\u0000]*/);
|
||||
|
||||
// Try to parse `>` or `/>`, mutating `tag` to be self-closing in the latter
|
||||
// case (and failing fatally if `/` isn't followed by `>`).
|
||||
// Return tag if successful.
|
||||
var handleEndOfTag = function (scanner, tag) {
|
||||
if (getClangle(scanner))
|
||||
return tag;
|
||||
|
||||
if (getSlash(scanner)) {
|
||||
if (! getClangle(scanner))
|
||||
scanner.fatal("Expected `>` after `/`");
|
||||
tag.isSelfClosing = true;
|
||||
return tag;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
var getQuotedAttributeValue = function (scanner, quote) {
|
||||
if (scanner.peek() !== quote)
|
||||
return null;
|
||||
scanner.pos++;
|
||||
|
||||
var tokens = [];
|
||||
var charsTokenToExtend = null;
|
||||
|
||||
var charRef;
|
||||
while (true) {
|
||||
var ch = scanner.peek();
|
||||
var special;
|
||||
var curPos = scanner.pos;
|
||||
if (ch === quote) {
|
||||
scanner.pos++;
|
||||
return tokens;
|
||||
} else if (! ch) {
|
||||
scanner.fatal("Unclosed quoted attribute in tag");
|
||||
} else if (ch === '\u0000') {
|
||||
scanner.fatal("Unexpected NULL character in attribute value");
|
||||
} else if (ch === '&' && (charRef = getCharacterReference(scanner, true, quote))) {
|
||||
tokens.push(charRef);
|
||||
charsTokenToExtend = null;
|
||||
} else if (scanner.getSpecialTag &&
|
||||
((special = scanner.getSpecialTag(scanner,
|
||||
TEMPLATE_TAG_POSITION.IN_ATTRIBUTE)) ||
|
||||
scanner.pos > curPos /* `{{! comment}}` */)) {
|
||||
// note: this code is messy because it turns out to be annoying for getSpecialTag
|
||||
// to return `null` when it scans a comment. Also, this code should be de-duped
|
||||
// with getUnquotedAttributeValue
|
||||
if (special) {
|
||||
tokens.push({t: 'Special', v: special});
|
||||
charsTokenToExtend = null;
|
||||
}
|
||||
} else {
|
||||
if (! charsTokenToExtend) {
|
||||
charsTokenToExtend = { t: 'Chars', v: '' };
|
||||
tokens.push(charsTokenToExtend);
|
||||
}
|
||||
charsTokenToExtend.v += (ch === '\r' ? '\n' : ch);
|
||||
scanner.pos++;
|
||||
if (ch === '\r' && scanner.peek() === '\n')
|
||||
scanner.pos++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var getUnquotedAttributeValue = function (scanner) {
|
||||
var tokens = [];
|
||||
var charsTokenToExtend = null;
|
||||
|
||||
var charRef;
|
||||
while (true) {
|
||||
var ch = scanner.peek();
|
||||
var special;
|
||||
var curPos = scanner.pos;
|
||||
if (HTML_SPACE.test(ch) || ch === '>') {
|
||||
return tokens;
|
||||
} else if (! ch) {
|
||||
scanner.fatal("Unclosed attribute in tag");
|
||||
} else if ('\u0000"\'<=`'.indexOf(ch) >= 0) {
|
||||
scanner.fatal("Unexpected character in attribute value");
|
||||
} else if (ch === '&' && (charRef = getCharacterReference(scanner, true, '>'))) {
|
||||
tokens.push(charRef);
|
||||
charsTokenToExtend = null;
|
||||
} else if (scanner.getSpecialTag &&
|
||||
((special = scanner.getSpecialTag(scanner,
|
||||
TEMPLATE_TAG_POSITION.IN_ATTRIBUTE)) ||
|
||||
scanner.pos > curPos /* `{{! comment}}` */)) {
|
||||
if (special) {
|
||||
tokens.push({t: 'Special', v: special});
|
||||
charsTokenToExtend = null;
|
||||
}
|
||||
} else {
|
||||
if (! charsTokenToExtend) {
|
||||
charsTokenToExtend = { t: 'Chars', v: '' };
|
||||
tokens.push(charsTokenToExtend);
|
||||
}
|
||||
charsTokenToExtend.v += ch;
|
||||
scanner.pos++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getTagToken = HTMLTools.Parse.getTagToken = function (scanner) {
|
||||
if (! (scanner.peek() === '<' && scanner.rest().charAt(1) !== '!'))
|
||||
return null;
|
||||
scanner.pos++;
|
||||
|
||||
var tag = { t: 'Tag' };
|
||||
|
||||
// now looking at the character after `<`, which is not a `!`
|
||||
if (scanner.peek() === '/') {
|
||||
tag.isEnd = true;
|
||||
scanner.pos++;
|
||||
}
|
||||
|
||||
var tagName = getTagName(scanner);
|
||||
if (! tagName)
|
||||
scanner.fatal("Expected tag name after `<`");
|
||||
tag.n = HTMLTools.properCaseTagName(tagName);
|
||||
|
||||
if (scanner.peek() === '/' && tag.isEnd)
|
||||
scanner.fatal("End tag can't have trailing slash");
|
||||
if (handleEndOfTag(scanner, tag))
|
||||
return tag;
|
||||
|
||||
if (scanner.isEOF())
|
||||
scanner.fatal("Unclosed `<`");
|
||||
|
||||
if (! HTML_SPACE.test(scanner.peek()))
|
||||
// e.g. `<a{{b}}>`
|
||||
scanner.fatal("Expected space after tag name");
|
||||
|
||||
// we're now in "Before attribute name state" of the tokenizer
|
||||
skipSpaces(scanner);
|
||||
|
||||
if (scanner.peek() === '/' && tag.isEnd)
|
||||
scanner.fatal("End tag can't have trailing slash");
|
||||
if (handleEndOfTag(scanner, tag))
|
||||
return tag;
|
||||
|
||||
if (tag.isEnd)
|
||||
scanner.fatal("End tag can't have attributes");
|
||||
|
||||
tag.attrs = {};
|
||||
|
||||
while (true) {
|
||||
// Note: at the top of this loop, we've already skipped any spaces.
|
||||
|
||||
// This will be set to true if after parsing the attribute, we should
|
||||
// require spaces (or else an end of tag, i.e. `>` or `/>`).
|
||||
var spacesRequiredAfter = false;
|
||||
|
||||
// first, try for a special tag.
|
||||
var curPos = scanner.pos;
|
||||
var special = (scanner.getSpecialTag &&
|
||||
scanner.getSpecialTag(scanner,
|
||||
TEMPLATE_TAG_POSITION.IN_START_TAG));
|
||||
if (special || (scanner.pos > curPos)) {
|
||||
if (special) {
|
||||
tag.attrs.$specials = (tag.attrs.$specials || []);
|
||||
tag.attrs.$specials.push({ t: 'Special', v: special });
|
||||
} // else, must have scanned a `{{! comment}}`
|
||||
|
||||
spacesRequiredAfter = true;
|
||||
} else {
|
||||
|
||||
var attributeName = getAttributeName(scanner);
|
||||
if (! attributeName)
|
||||
scanner.fatal("Expected attribute name in tag");
|
||||
// Throw error on `{` in attribute name. This provides *some* error message
|
||||
// if someone writes `<a x{{y}}>` or `<a x{{y}}=z>`. The HTML tokenization
|
||||
// spec doesn't say that `{` is invalid, but the DOM API (setAttribute) won't
|
||||
// allow it, so who cares.
|
||||
if (attributeName.indexOf('{') >= 0)
|
||||
scanner.fatal("Unexpected `{` in attribute name.");
|
||||
attributeName = HTMLTools.properCaseAttributeName(attributeName);
|
||||
|
||||
if (tag.attrs.hasOwnProperty(attributeName))
|
||||
scanner.fatal("Duplicate attribute in tag: " + attributeName);
|
||||
|
||||
tag.attrs[attributeName] = [];
|
||||
|
||||
skipSpaces(scanner);
|
||||
|
||||
if (handleEndOfTag(scanner, tag))
|
||||
return tag;
|
||||
|
||||
var ch = scanner.peek();
|
||||
if (! ch)
|
||||
scanner.fatal("Unclosed <");
|
||||
if ('\u0000"\'<'.indexOf(ch) >= 0)
|
||||
scanner.fatal("Unexpected character after attribute name in tag");
|
||||
|
||||
if (ch === '=') {
|
||||
scanner.pos++;
|
||||
|
||||
skipSpaces(scanner);
|
||||
|
||||
ch = scanner.peek();
|
||||
if (! ch)
|
||||
scanner.fatal("Unclosed <");
|
||||
if ('\u0000><=`'.indexOf(ch) >= 0)
|
||||
scanner.fatal("Unexpected character after = in tag");
|
||||
|
||||
if ((ch === '"') || (ch === "'"))
|
||||
tag.attrs[attributeName] = getQuotedAttributeValue(scanner, ch);
|
||||
else
|
||||
tag.attrs[attributeName] = getUnquotedAttributeValue(scanner);
|
||||
|
||||
spacesRequiredAfter = true;
|
||||
}
|
||||
}
|
||||
// now we are in the "post-attribute" position, whether it was a special attribute
|
||||
// (like `{{x}}`) or a normal one (like `x` or `x=y`).
|
||||
|
||||
if (handleEndOfTag(scanner, tag))
|
||||
return tag;
|
||||
|
||||
if (scanner.isEOF())
|
||||
scanner.fatal("Unclosed `<`");
|
||||
|
||||
if (spacesRequiredAfter)
|
||||
requireSpaces(scanner);
|
||||
else
|
||||
skipSpaces(scanner);
|
||||
|
||||
if (handleEndOfTag(scanner, tag))
|
||||
return tag;
|
||||
}
|
||||
};
|
||||
|
||||
TEMPLATE_TAG_POSITION = HTMLTools.TEMPLATE_TAG_POSITION = {
|
||||
ELEMENT: 1,
|
||||
IN_START_TAG: 2,
|
||||
IN_ATTRIBUTE: 3,
|
||||
IN_RCDATA: 4,
|
||||
IN_RAWTEXT: 5
|
||||
};
|
||||
|
||||
// tagName must be proper case
|
||||
isLookingAtEndTag = function (scanner, tagName) {
|
||||
var rest = scanner.rest();
|
||||
var pos = 0; // into rest
|
||||
var firstPart = /^<\/([a-zA-Z]+)/.exec(rest);
|
||||
if (firstPart &&
|
||||
HTMLTools.properCaseTagName(firstPart[1]) === tagName) {
|
||||
// we've seen `</foo`, now see if the end tag continues
|
||||
pos += firstPart[0].length;
|
||||
while (pos < rest.length && HTML_SPACE.test(rest.charAt(pos)))
|
||||
pos++;
|
||||
if (pos < rest.length && rest.charAt(pos) === '>')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
344
packages/html-tools/tokenize_tests.js
Normal file
344
packages/html-tools/tokenize_tests.js
Normal file
@@ -0,0 +1,344 @@
|
||||
var Scanner = HTMLTools.Scanner;
|
||||
var getComment = HTMLTools.Parse.getComment;
|
||||
var getDoctype = HTMLTools.Parse.getDoctype;
|
||||
var getHTMLToken = HTMLTools.Parse.getHTMLToken;
|
||||
|
||||
// "tokenize" is not really a great operation for real use, because
|
||||
// it ignores the special content rules for tags like "style" and
|
||||
// "script".
|
||||
var tokenize = function (input) {
|
||||
var scanner = new Scanner(input);
|
||||
var tokens = [];
|
||||
while (! scanner.isEOF()) {
|
||||
var token = getHTMLToken(scanner);
|
||||
if (token)
|
||||
tokens.push(token);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
};
|
||||
|
||||
|
||||
Tinytest.add("html-tools - comments", function (test) {
|
||||
var succeed = function (input, content) {
|
||||
var scanner = new Scanner(input);
|
||||
var result = getComment(scanner);
|
||||
test.isTrue(result);
|
||||
test.equal(scanner.pos, content.length + 7);
|
||||
test.equal(result, {
|
||||
t: 'Comment',
|
||||
v: content
|
||||
});
|
||||
};
|
||||
|
||||
var ignore = function (input) {
|
||||
var scanner = new Scanner(input);
|
||||
var result = getComment(scanner);;
|
||||
test.isFalse(result);
|
||||
test.equal(scanner.pos, 0);
|
||||
};
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
var scanner = new Scanner(input);
|
||||
var error;
|
||||
try {
|
||||
getComment(scanner);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (error)
|
||||
test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
test.equal(getComment(new Scanner("<!-- hello -->")),
|
||||
{ t: 'Comment', v: ' hello ' });
|
||||
|
||||
ignore("<!DOCTYPE>");
|
||||
ignore("<!-a");
|
||||
ignore("<--");
|
||||
ignore("<!");
|
||||
ignore("abc");
|
||||
ignore("<a");
|
||||
|
||||
fatal('<!--', 'Unclosed');
|
||||
fatal('<!---', 'Unclosed');
|
||||
fatal('<!----', 'Unclosed');
|
||||
fatal('<!-- -', 'Unclosed');
|
||||
fatal('<!-- --', 'Unclosed');
|
||||
fatal('<!-- -- abcd', 'Unclosed');
|
||||
fatal('<!-- ->', 'Unclosed');
|
||||
fatal('<!-- a--b -->', 'cannot contain');
|
||||
fatal('<!--x--->', 'must end at first');
|
||||
|
||||
fatal('<!-- a\u0000b -->', 'cannot contain');
|
||||
fatal('<!--\u0000 x-->', 'cannot contain');
|
||||
|
||||
succeed('<!---->', '');
|
||||
succeed('<!---x-->', '-x');
|
||||
succeed('<!--x-->', 'x');
|
||||
succeed('<!-- hello - - world -->', ' hello - - world ');
|
||||
});
|
||||
|
||||
Tinytest.add("html-tools - doctype", function (test) {
|
||||
var succeed = function (input, expectedProps) {
|
||||
var scanner = new Scanner(input);
|
||||
var result = getDoctype(scanner);
|
||||
test.isTrue(result);
|
||||
test.equal(scanner.pos, result.v.length);
|
||||
test.equal(input.slice(0, result.v.length), result.v);
|
||||
var actualProps = _.extend({}, result);
|
||||
delete actualProps.t;
|
||||
delete actualProps.v;
|
||||
test.equal(actualProps, expectedProps);
|
||||
};
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
var scanner = new Scanner(input);
|
||||
var error;
|
||||
try {
|
||||
getDoctype(scanner);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (messageContains)
|
||||
test.isTrue(error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
test.equal(getDoctype(new Scanner("<!DOCTYPE html>x")),
|
||||
{ t: 'Doctype',
|
||||
v: '<!DOCTYPE html>',
|
||||
name: 'html' });
|
||||
|
||||
test.equal(getDoctype(new Scanner("<!DOCTYPE html SYSTEM 'about:legacy-compat'>x")),
|
||||
{ t: 'Doctype',
|
||||
v: "<!DOCTYPE html SYSTEM 'about:legacy-compat'>",
|
||||
name: 'html',
|
||||
systemId: 'about:legacy-compat' });
|
||||
|
||||
test.equal(getDoctype(new Scanner("<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN'>x")),
|
||||
{ t: 'Doctype',
|
||||
v: "<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN'>",
|
||||
name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN' });
|
||||
|
||||
test.equal(getDoctype(new Scanner("<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN' 'http://www.w3.org/TR/html4/strict.dtd'>x")),
|
||||
{ t: 'Doctype',
|
||||
v: "<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN' 'http://www.w3.org/TR/html4/strict.dtd'>",
|
||||
name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN',
|
||||
systemId: 'http://www.w3.org/TR/html4/strict.dtd' });
|
||||
|
||||
succeed('<!DOCTYPE html>', {name: 'html'});
|
||||
succeed('<!DOCTYPE htML>', {name: 'html'});
|
||||
succeed('<!DOCTYPE HTML>', {name: 'html'});
|
||||
succeed('<!doctype html>', {name: 'html'});
|
||||
succeed('<!doctYPE html>', {name: 'html'});
|
||||
succeed('<!DOCTYPE html \u000c>', {name: 'html'});
|
||||
fatal('<!DOCTYPE', 'Expected space');
|
||||
fatal('<!DOCTYPE ', 'Malformed DOCTYPE');
|
||||
fatal('<!DOCTYPE ', 'Malformed DOCTYPE');
|
||||
fatal('<!DOCTYPE>', 'Expected space');
|
||||
fatal('<!DOCTYPE >', 'Malformed DOCTYPE');
|
||||
fatal('<!DOCTYPE\u0000', 'Expected space');
|
||||
fatal('<!DOCTYPE \u0000', 'Malformed DOCTYPE');
|
||||
fatal('<!DOCTYPE html\u0000>', 'Malformed DOCTYPE');
|
||||
fatal('<!DOCTYPE html', 'Malformed DOCTYPE');
|
||||
|
||||
succeed('<!DOCTYPE html SYSTEM "about:legacy-compat">', {name: 'html', systemId: 'about:legacy-compat'});
|
||||
succeed('<!doctype HTML system "about:legacy-compat">', {name: 'html', systemId: 'about:legacy-compat'});
|
||||
succeed("<!DOCTYPE html SYSTEM 'about:legacy-compat'>", {name: 'html', systemId: 'about:legacy-compat'});
|
||||
succeed("<!dOcTyPe HtMl sYsTeM 'about:legacy-compat'>", {name: 'html', systemId: 'about:legacy-compat'});
|
||||
succeed('<!DOCTYPE html\tSYSTEM\t"about:legacy-compat" \t>', {name: 'html', systemId: 'about:legacy-compat'});
|
||||
fatal('<!DOCTYPE html SYSTE "about:legacy-compat">', 'Expected PUBLIC or SYSTEM');
|
||||
fatal('<!DOCTYPE html SYSTE', 'Expected PUBLIC or SYSTEM');
|
||||
fatal('<!DOCTYPE html SYSTEM"about:legacy-compat">', 'Expected space');
|
||||
fatal('<!DOCTYPE html SYSTEM');
|
||||
fatal('<!DOCTYPE html SYSTEM ');
|
||||
fatal('<!DOCTYPE html SYSTEM>');
|
||||
fatal('<!DOCTYPE html SYSTEM >');
|
||||
fatal('<!DOCTYPE html SYSTEM ">">');
|
||||
fatal('<!DOCTYPE html SYSTEM "\u0000about:legacy-compat">');
|
||||
fatal('<!DOCTYPE html SYSTEM "about:legacy-compat\u0000">');
|
||||
fatal('<!DOCTYPE html SYSTEM "');
|
||||
fatal('<!DOCTYPE html SYSTEM "">');
|
||||
fatal('<!DOCTYPE html SYSTEM \'');
|
||||
fatal('<!DOCTYPE html SYSTEM\'a\'>');
|
||||
fatal('<!DOCTYPE html SYSTEM about:legacy-compat>');
|
||||
|
||||
succeed('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN">',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN'});
|
||||
succeed('<!DOCTYPE html PUBLIC \'-//W3C//DTD HTML 4.0//EN\'>',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN'});
|
||||
succeed('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN',
|
||||
systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
|
||||
succeed('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN" \'http://www.w3.org/TR/REC-html40/strict.dtd\'>',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN',
|
||||
systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
|
||||
succeed('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd\'>',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN',
|
||||
systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
|
||||
succeed('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\'\t\'http://www.w3.org/TR/REC-html40/strict.dtd\' >',
|
||||
{ name: 'html',
|
||||
publicId: '-//W3C//DTD HTML 4.0//EN',
|
||||
systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
|
||||
fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd\'');
|
||||
fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd\'');
|
||||
fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd');
|
||||
fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'');
|
||||
fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' ');
|
||||
fatal('<!DOCTYPE html public \'- ');
|
||||
fatal('<!DOCTYPE html public>');
|
||||
fatal('<!DOCTYPE html public "-//W3C//DTD HTML 4.0//EN""http://www.w3.org/TR/REC-html40/strict.dtd">');
|
||||
});
|
||||
|
||||
Tinytest.add("html-tools - tokenize", function (test) {
|
||||
|
||||
var fatal = function (input, messageContains) {
|
||||
var error;
|
||||
try {
|
||||
tokenize(input);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
test.isTrue(error);
|
||||
if (messageContains)
|
||||
test.isTrue(error.message.indexOf(messageContains) >= 0, error.message);
|
||||
};
|
||||
|
||||
|
||||
test.equal(tokenize(''), []);
|
||||
test.equal(tokenize('abc'), [{t: 'Chars', v: 'abc'}]);
|
||||
test.equal(tokenize('&'), [{t: 'Chars', v: '&'}]);
|
||||
test.equal(tokenize('&'), [{t: 'CharRef', v: '&', cp: [38]}]);
|
||||
test.equal(tokenize('ok fine'),
|
||||
[{t: 'Chars', v: 'ok'},
|
||||
{t: 'CharRef', v: ' ', cp: [32]},
|
||||
{t: 'Chars', v: 'fine'}]);
|
||||
|
||||
test.equal(tokenize('a<!--b-->c'),
|
||||
[{t: 'Chars',
|
||||
v: 'a'},
|
||||
{t: 'Comment',
|
||||
v: 'b'},
|
||||
{t: 'Chars',
|
||||
v: 'c'}]);
|
||||
|
||||
test.equal(tokenize('<a>'), [{t: 'Tag', n: 'a'}]);
|
||||
|
||||
fatal('<');
|
||||
fatal('<x');
|
||||
fatal('<x ');
|
||||
fatal('<x a');
|
||||
fatal('<x a ');
|
||||
fatal('<x a =');
|
||||
fatal('<x a = ');
|
||||
fatal('<x a = b');
|
||||
fatal('<x a = "b');
|
||||
fatal('<x a = \'b');
|
||||
fatal('<x a = b ');
|
||||
fatal('<x a = b /');
|
||||
test.equal(tokenize('<x a = b />'),
|
||||
[{t: 'Tag', n: 'x',
|
||||
attrs: { a: [{t: 'Chars', v: 'b'}] },
|
||||
isSelfClosing: true}]);
|
||||
|
||||
test.equal(tokenize('<a>X</a>'),
|
||||
[{t: 'Tag', n: 'a'},
|
||||
{t: 'Chars', v: 'X'},
|
||||
{t: 'Tag', n: 'a', isEnd: true}]);
|
||||
|
||||
fatal('<x a a>'); // duplicate attribute value
|
||||
test.equal(tokenize('<a b >'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [] }}]);
|
||||
fatal('< a>');
|
||||
fatal('< /a>');
|
||||
fatal('</ a>');
|
||||
|
||||
// Slash does not end an unquoted attribute, interestingly
|
||||
test.equal(tokenize('<a b=/>'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '/'}] }}]);
|
||||
|
||||
test.equal(tokenize('<a b="c" d=e f=\'g\' h \t>'),
|
||||
[{t: 'Tag', n: 'a',
|
||||
attrs: { b: [{t: 'Chars', v: 'c'}],
|
||||
d: [{t: 'Chars', v: 'e'}],
|
||||
f: [{t: 'Chars', v: 'g'}],
|
||||
h: [] }}]);
|
||||
|
||||
fatal('</a b="c" d=e f=\'g\' h \t\u0000>');
|
||||
fatal('</a b="c" d=ef=\'g\' h \t>');
|
||||
fatal('</a b="c"d=e f=\'g\' h \t>');
|
||||
|
||||
test.equal(tokenize('<a/>'), [{t: 'Tag', n: 'a', isSelfClosing: true}]);
|
||||
|
||||
fatal('<a/ >');
|
||||
fatal('<a/b>');
|
||||
fatal('<a b=c`>');
|
||||
fatal('<a b=c<>');
|
||||
|
||||
test.equal(tokenize('<a# b0="c@" d1=e2 f#=\'g \' h \t>'),
|
||||
[{t: 'Tag', n: 'a#',
|
||||
attrs: { b0: [{t: 'Chars', v: 'c@'}],
|
||||
d1: [{t: 'Chars', v: 'e2'}],
|
||||
'f#': [{t: 'Chars', v: 'g '}],
|
||||
h: [] }}]);
|
||||
|
||||
test.equal(tokenize('<div class=""></div>'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [] }},
|
||||
{t: 'Tag', n: 'div', isEnd: true}]);
|
||||
|
||||
test.equal(tokenize('<div class="&">'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [{t: 'Chars', v: '&'}] }}]);
|
||||
test.equal(tokenize('<div class=&>'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [{t: 'Chars', v: '&'}] }}]);
|
||||
test.equal(tokenize('<div class=&>'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [{t: 'CharRef', v: '&', cp: [38]}] }}]);
|
||||
|
||||
test.equal(tokenize('<div class=aa&𝕫∾̳&bb>'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [
|
||||
{t: 'Chars', v: 'aa&'},
|
||||
{t: 'CharRef', v: '𝕫', cp: [120171]},
|
||||
{t: 'CharRef', v: '∾̳', cp: [8766, 819]},
|
||||
{t: 'Chars', v: '&bb'}
|
||||
] }}]);
|
||||
|
||||
test.equal(tokenize('<div class="aa &𝕫∾̳& bb">'),
|
||||
[{t: 'Tag', n: 'div', attrs: { 'class': [
|
||||
{t: 'Chars', v: 'aa &'},
|
||||
{t: 'CharRef', v: '𝕫', cp: [120171]},
|
||||
{t: 'CharRef', v: '∾̳', cp: [8766, 819]},
|
||||
{t: 'Chars', v: '& bb'}
|
||||
] }}]);
|
||||
|
||||
test.equal(tokenize('<a b="\'`<>&">'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '\'`<>&'}] }}]);
|
||||
test.equal(tokenize('<a b=\'"`<>&\'>'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '"`<>&'}] }}]);
|
||||
|
||||
fatal('>');
|
||||
fatal('>c');
|
||||
test.equal(tokenize('<a b=>c>'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '>c' }] }}]);
|
||||
test.equal(tokenize('<a b=">c">'),
|
||||
[{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '>c' }] }}]);
|
||||
fatal('<a b=>>');
|
||||
fatal('<a b=">">');
|
||||
fatal('<a b=">=">');
|
||||
|
||||
fatal('<!');
|
||||
fatal('<!x>');
|
||||
|
||||
fatal('<a{{b}}>');
|
||||
fatal('<{{a}}>');
|
||||
fatal('</a b=c>'); // end tag can't have attributes
|
||||
fatal('</a/>'); // end tag can't be self-closing
|
||||
fatal('</a />');
|
||||
});
|
||||
50
packages/html-tools/utils.js
Normal file
50
packages/html-tools/utils.js
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
HTMLTools = {};
|
||||
HTMLTools.Parse = {};
|
||||
|
||||
var asciiLowerCase = HTMLTools.asciiLowerCase = function (str) {
|
||||
return str.replace(/[A-Z]/g, function (c) {
|
||||
return String.fromCharCode(c.charCodeAt(0) + 32);
|
||||
});
|
||||
};
|
||||
|
||||
var svgCamelCaseAttributes = 'attributeName attributeType baseFrequency baseProfile calcMode clipPathUnits contentScriptType contentStyleType diffuseConstant edgeMode externalResourcesRequired filterRes filterUnits glyphRef glyphRef gradientTransform gradientTransform gradientUnits gradientUnits kernelMatrix kernelUnitLength kernelUnitLength kernelUnitLength keyPoints keySplines keyTimes lengthAdjust limitingConeAngle markerHeight markerUnits markerWidth maskContentUnits maskUnits numOctaves pathLength patternContentUnits patternTransform patternUnits pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits refX refY repeatCount repeatDur requiredExtensions requiredFeatures specularConstant specularExponent specularExponent spreadMethod spreadMethod startOffset stdDeviation stitchTiles surfaceScale surfaceScale systemLanguage tableValues targetX targetY textLength textLength viewBox viewTarget xChannelSelector yChannelSelector zoomAndPan'.split(' ');
|
||||
|
||||
var properAttributeCaseMap = (function (map) {
|
||||
for (var i = 0; i < svgCamelCaseAttributes.length; i++) {
|
||||
var a = svgCamelCaseAttributes[i];
|
||||
map[asciiLowerCase(a)] = a;
|
||||
}
|
||||
return map;
|
||||
})({});
|
||||
|
||||
var properTagCaseMap = (function (map) {
|
||||
var knownElements = HTML.knownElementNames;
|
||||
for (var i = 0; i < knownElements.length; i++) {
|
||||
var a = knownElements[i];
|
||||
map[asciiLowerCase(a)] = a;
|
||||
}
|
||||
return map;
|
||||
})({});
|
||||
|
||||
// Take a tag name in any case and make it the proper case for HTML.
|
||||
//
|
||||
// Modern browsers let you embed SVG in HTML, but SVG elements are special
|
||||
// in that they have a case-sensitive DOM API (nodeName, getAttribute,
|
||||
// setAttribute). For example, it has to be `setAttribute("viewBox")`,
|
||||
// not `"viewbox"`. However, the browser's HTML parser is NOT case sensitive
|
||||
// and will fix the case for you, so if you write `<svg viewbox="...">`
|
||||
// you actually get a `"viewBox"` attribute. Any HTML-parsing toolchain
|
||||
// must do the same.
|
||||
HTMLTools.properCaseTagName = function (name) {
|
||||
var lowered = asciiLowerCase(name);
|
||||
return properTagCaseMap.hasOwnProperty(lowered) ?
|
||||
properTagCaseMap[lowered] : lowered;
|
||||
};
|
||||
|
||||
// See docs for properCaseTagName.
|
||||
HTMLTools.properCaseAttributeName = function (name) {
|
||||
var lowered = asciiLowerCase(name);
|
||||
return properAttributeCaseMap.hasOwnProperty(lowered) ?
|
||||
properAttributeCaseMap[lowered] : lowered;
|
||||
};
|
||||
1
packages/html5-tokenizer/.gitignore
vendored
Normal file
1
packages/html5-tokenizer/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
3
packages/html5-tokenizer/README.md
Normal file
3
packages/html5-tokenizer/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
Code comes from https://github.com/aredridel/html5 a.k.a. `npm html5`, but
|
||||
just the tokenizer, no parsing and no `jsdom` dependency.
|
||||
90
packages/html5-tokenizer/buffer.js
Normal file
90
packages/html5-tokenizer/buffer.js
Normal file
@@ -0,0 +1,90 @@
|
||||
var Buffer = HTML5.Buffer = function Buffer() {
|
||||
this.data = '';
|
||||
this.start = 0;
|
||||
this.committed = 0;
|
||||
this.eof = false;
|
||||
};
|
||||
|
||||
Buffer.prototype = {
|
||||
slice: function() {
|
||||
if(this.start >= this.data.length) {
|
||||
if(!this.eof) throw HTML5.DRAIN;
|
||||
return HTML5.EOF;
|
||||
}
|
||||
return this.data.slice(this.start, this.data.length);
|
||||
},
|
||||
char: function() {
|
||||
if(!this.eof && this.start >= this.data.length - 1) throw HTML5.DRAIN;
|
||||
if(this.start >= this.data.length) {
|
||||
return HTML5.EOF;
|
||||
}
|
||||
return this.data[this.start++];
|
||||
},
|
||||
advance: function(amount) {
|
||||
this.start += amount;
|
||||
if(this.start >= this.data.length) {
|
||||
if(!this.eof) throw HTML5.DRAIN;
|
||||
return HTML5.EOF;
|
||||
} else {
|
||||
if(this.committed > this.data.length / 2) {
|
||||
// Sliiiide
|
||||
this.data = this.data.slice(this.committed);
|
||||
this.start = this.start - this.committed;
|
||||
this.committed = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
matchWhile: function(re) {
|
||||
if(this.eof && this.start >= this.data.length ) return '';
|
||||
var r = new RegExp("^"+re+"+");
|
||||
var m = r.exec(this.slice());
|
||||
if(m) {
|
||||
if(!this.eof && m[0].length == this.data.length - this.start) throw HTML5.DRAIN;
|
||||
this.advance(m[0].length);
|
||||
return m[0];
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
matchUntil: function(re) {
|
||||
var m, s;
|
||||
s = this.slice();
|
||||
if(s === HTML5.EOF) {
|
||||
return '';
|
||||
} else if(m = new RegExp(re + (this.eof ? "|\0|$" : "|\0")).exec(this.slice())) {
|
||||
var t = this.data.slice(this.start, this.start + m.index);
|
||||
this.advance(m.index);
|
||||
return t.toString();
|
||||
} else {
|
||||
throw HTML5.DRAIN;
|
||||
}
|
||||
},
|
||||
append: function(data) {
|
||||
this.data += data;
|
||||
},
|
||||
shift: function(n) {
|
||||
if(!this.eof && this.start + n >= this.data.length) throw HTML5.DRAIN;
|
||||
if(this.eof && this.start >= this.data.length) return HTML5.EOF;
|
||||
var d = this.data.slice(this.start, this.start + n).toString();
|
||||
this.advance(Math.min(n, this.data.length - this.start));
|
||||
return d;
|
||||
},
|
||||
peek: function(n) {
|
||||
if(!this.eof && this.start + n >= this.data.length) throw HTML5.DRAIN;
|
||||
if(this.eof && this.start >= this.data.length) return HTML5.EOF;
|
||||
return this.data.slice(this.start, Math.min(this.start + n, this.data.length)).toString();
|
||||
},
|
||||
length: function() {
|
||||
return this.data.length - this.start - 1;
|
||||
},
|
||||
unget: function(d) {
|
||||
if(d === HTML5.EOF) return;
|
||||
this.start -= (d.length);
|
||||
},
|
||||
undo: function() {
|
||||
this.start = this.committed;
|
||||
},
|
||||
commit: function() {
|
||||
this.committed = this.start;
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user