FIX: Various bugs with Category breadcrumbs
This commit is contained in:
parent
4e46d91b8d
commit
e9c4465ec7
|
@ -12,8 +12,11 @@ Discourse.DiscourseBreadcrumbsComponent = Ember.Component.extend({
|
|||
return this.get('parentCategory') || this.get('category');
|
||||
}.property('parentCategory', 'category'),
|
||||
|
||||
childCategories: Em.computed.filter('categories', function(c) {
|
||||
return c.get('parentCategory') === this.get('targetCategory');
|
||||
})
|
||||
childCategories: function() {
|
||||
var self = this;
|
||||
return this.get('categories').filter(function (c) {
|
||||
return c.get('parentCategory') === self.get('targetCategory');
|
||||
});
|
||||
}.property('targetCategory')
|
||||
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
Discourse.DiscourseCategorydropComponent = Ember.Component.extend({
|
||||
|
||||
classNameBindings: ['category::no-category', 'categories:has-drop'],
|
||||
tagName: 'li',
|
||||
|
||||
|
@ -8,6 +7,15 @@ Discourse.DiscourseCategorydropComponent = Ember.Component.extend({
|
|||
return "icon icon-caret-right";
|
||||
}.property('expanded'),
|
||||
|
||||
badgeStyle: function() {
|
||||
var category = this.get('category');
|
||||
if (category) {
|
||||
return Discourse.HTML.categoryStyle(category);
|
||||
} else {
|
||||
return "background-color: #eee; color: #333";
|
||||
}
|
||||
}.property('category'),
|
||||
|
||||
actions: {
|
||||
expand: function() {
|
||||
if (this.get('expanded')) {
|
||||
|
@ -18,25 +26,27 @@ Discourse.DiscourseCategorydropComponent = Ember.Component.extend({
|
|||
if (this.get('categories')) {
|
||||
this.set('expanded', true);
|
||||
}
|
||||
|
||||
var self = this,
|
||||
$dropdown = this.$()[0];
|
||||
|
||||
$('html').on('click.category-drop', function(e) {
|
||||
var closest = $(e.target).closest($dropdown);
|
||||
return (closest.length && closest[0] === $dropdown) ? true : self.close();
|
||||
var $target = $(e.target),
|
||||
closest = $target.closest($dropdown);
|
||||
|
||||
return ($(e.currentTarget).hasClass('badge-category') || (closest.length && closest[0] === $dropdown)) ? true : self.close();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
categoryChanged: function() {
|
||||
this.close();
|
||||
}.observes('category', 'parentCategory'),
|
||||
|
||||
close: function() {
|
||||
$('html').off('click.category-drop');
|
||||
this.set('expanded', false);
|
||||
},
|
||||
|
||||
didInsertElement: function() {
|
||||
},
|
||||
|
||||
willDestroyElement: function() {
|
||||
$('html').off('click.category-drop');
|
||||
}
|
||||
|
|
|
@ -29,12 +29,32 @@ Handlebars.registerHelper('shorten', function(property, options) {
|
|||
@for Handlebars
|
||||
**/
|
||||
Handlebars.registerHelper('topicLink', function(property, options) {
|
||||
var title, topic;
|
||||
topic = Ember.Handlebars.get(this, property, options);
|
||||
var topic = Ember.Handlebars.get(this, property, options),
|
||||
title = topic.get('fancy_title') || topic.get('title');
|
||||
return "<a href='" + (topic.get('lastUnreadUrl')) + "' class='title'>" + title + "</a>";
|
||||
return "<a href='" + topic.get('lastUnreadUrl') + "' class='title'>" + title + "</a>";
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
Produces a link to a category given a category object and helper options
|
||||
|
||||
@method categoryLinkHTML
|
||||
@param {Discourse.Category} category to link to
|
||||
@param {Object} options standard from handlebars
|
||||
**/
|
||||
function categoryLinkHTML(category, options) {
|
||||
var categoryOptions = {};
|
||||
if (options.hash) {
|
||||
if (options.hash.allowUncategorized) {
|
||||
categoryOptions.allowUncategorized = true;
|
||||
}
|
||||
if (options.hash.categories) {
|
||||
categoryOptions.categories = Em.Handlebars.get(this, options.hash.categories, options);
|
||||
}
|
||||
}
|
||||
return new Handlebars.SafeString(Discourse.HTML.categoryLink(category, categoryOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
Produces a link to a category
|
||||
|
||||
|
@ -42,21 +62,16 @@ Handlebars.registerHelper('topicLink', function(property, options) {
|
|||
@for Handlebars
|
||||
**/
|
||||
Handlebars.registerHelper('categoryLink', function(property, options) {
|
||||
var allowUncategorized = options.hash && options.hash.allowUncategorized;
|
||||
var category = Ember.Handlebars.get(this, property, options);
|
||||
return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category, allowUncategorized));
|
||||
return categoryLinkHTML(Ember.Handlebars.get(this, property, options), options);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
Produces a bound link to a category
|
||||
|
||||
@method boundCategoryLink
|
||||
@for Handlebars
|
||||
**/
|
||||
Ember.Handlebars.registerBoundHelper('boundCategoryLink', function(category) {
|
||||
return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category));
|
||||
});
|
||||
Ember.Handlebars.registerBoundHelper('boundCategoryLink', categoryLinkHTML);
|
||||
|
||||
/**
|
||||
Produces a link to a route with support for i18n on the title
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
Helpers to build HTML strings such as rich links to categories and topics.
|
||||
|
||||
@class HTML
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.HTML = {
|
||||
|
||||
/**
|
||||
Returns the CSS styles for a category
|
||||
|
||||
@method categoryStyle
|
||||
@param {Discourse.Category} category the category whose link we want
|
||||
**/
|
||||
categoryStyle: function(category) {
|
||||
var color = Em.get(category, 'color'),
|
||||
textColor = Em.get(category, 'text_color');
|
||||
|
||||
if (!color && !textColor) { return; }
|
||||
|
||||
// Add the custom style if we need to
|
||||
var style = "";
|
||||
if (color) { style += "background-color: #" + color + "; "; }
|
||||
if (textColor) { style += "color: #" + textColor + "; "; }
|
||||
return style;
|
||||
},
|
||||
|
||||
/**
|
||||
Create a badge-like category link
|
||||
|
||||
@method categoryLink
|
||||
@param {Discourse.Category} category the category whose link we want
|
||||
@param {Object} opts The options for the category link
|
||||
@param {Boolean} opts.allowUncategorized Whether we allow rendering of the uncategorized category
|
||||
@returns {String} the html category badge
|
||||
**/
|
||||
categoryLink: function(category, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
if ((!category) ||
|
||||
(!opts.allowUncategorized && Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id"))) return "";
|
||||
|
||||
var name = Em.get(category, 'name'),
|
||||
description = Em.get(category, 'description'),
|
||||
html = "<a href=\"" + Discourse.getURL("/category/") + Discourse.Category.slugFor(category) + "\" class=\"badge-category\" ";
|
||||
|
||||
// Add description if we have it
|
||||
if (description) html += "title=\"" + Handlebars.Utils.escapeExpression(description) + "\" ";
|
||||
|
||||
var categoryStyle = Discourse.HTML.categoryStyle(category);
|
||||
if (categoryStyle) {
|
||||
html += "style=\"" + categoryStyle + "\" ";
|
||||
}
|
||||
html += ">" + name + "</a>";
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
};
|
|
@ -33,29 +33,6 @@ Discourse.Utilities = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Create a badge-like category link
|
||||
|
||||
@method categoryLink
|
||||
@param {Discourse.Category} category the category whose link we want
|
||||
@returns {String} the html category badge
|
||||
**/
|
||||
categoryLink: function(category, allowUncategorized) {
|
||||
if (!category) return "";
|
||||
if (!allowUncategorized && Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id")) return "";
|
||||
|
||||
var color = Em.get(category, 'color'),
|
||||
textColor = Em.get(category, 'text_color'),
|
||||
name = Em.get(category, 'name'),
|
||||
description = Em.get(category, 'description'),
|
||||
html = "<a href=\"" + Discourse.getURL("/category/") + Discourse.Category.slugFor(category) + "\" class=\"badge-category\" ";
|
||||
|
||||
// Add description if we have it
|
||||
if (description) html += "title=\"" + Handlebars.Utils.escapeExpression(description) + "\" ";
|
||||
|
||||
return html + "style=\"background-color: #" + color + "; color: #" + textColor + ";\">" + name + "</a>";
|
||||
},
|
||||
|
||||
avatarUrl: function(template, size) {
|
||||
if (!template) { return ""; }
|
||||
var rawSize = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize(size));
|
||||
|
|
|
@ -43,7 +43,11 @@ Discourse.NavItem = Discourse.Model.extend({
|
|||
var mode = "";
|
||||
var category = this.get("category");
|
||||
if(category){
|
||||
mode += "category/" + category.get("slug") + "/";
|
||||
mode += "category/";
|
||||
|
||||
var parentSlug = category.get('parentCategory.slug');
|
||||
if (parentSlug) { mode += parentSlug + "/"; }
|
||||
mode += category.get("slug") + "/l/";
|
||||
}
|
||||
return mode + name.replace(' ', '-');
|
||||
}
|
||||
|
|
|
@ -26,8 +26,10 @@ Discourse.Route.buildRoutes(function() {
|
|||
Discourse.ListController.filters.forEach(function(filter) {
|
||||
router.route(filter, { path: "/" + filter });
|
||||
router.route(filter, { path: "/" + filter + "/more" });
|
||||
router.route(filter + "Category", { path: "/category/:slug/" + filter });
|
||||
router.route(filter + "Category", { path: "/category/:slug/" + filter + "/more" });
|
||||
router.route(filter + "Category", { path: "/category/:slug/l/" + filter });
|
||||
router.route(filter + "Category", { path: "/category/:slug/l/" + filter + "/more" });
|
||||
router.route(filter + "Category", { path: "/category/:parentSlug/:slug/l/" + filter });
|
||||
router.route(filter + "Category", { path: "/category/:parentSlug/:slug/l/" + filter + "/more" });
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -23,10 +23,9 @@ Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
|
|||
|
||||
var listController = this.controllerFor('list'),
|
||||
categorySlug = Discourse.Category.slugFor(category),
|
||||
self = this;
|
||||
|
||||
var filter = this.filter || "latest";
|
||||
var url = "category/" + categorySlug + "/" + filter;
|
||||
self = this,
|
||||
filter = this.filter || "latest",
|
||||
url = "category/" + categorySlug + "/l/" + filter;
|
||||
|
||||
listController.set('filterMode', url);
|
||||
listController.load(url).then(function(topicList) {
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
{{discourse-categorydrop title=title categories=parentCategories}}
|
||||
|
||||
{{discourse-categorydrop category=targetCategory categories=childCategories}}
|
||||
|
||||
<li>
|
||||
{{discourse-categorydrop parentCategory=category categories=parentCategories}}
|
||||
</li>
|
||||
<li>
|
||||
{{discourse-categorydrop parentCategory=category category=targetCategory categories=childCategories}}
|
||||
</li>
|
||||
{{#if parentCategory}}
|
||||
{{discourse-categorydrop category=category}}
|
||||
<li>
|
||||
{{boundCategoryLink category}}
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
<div class='clear'></div>
|
|
@ -1,13 +1,12 @@
|
|||
{{#if category}}
|
||||
{{categoryLink category}}
|
||||
{{boundCategoryLink category}}
|
||||
{{else}}
|
||||
<a href="/"><i class='icon icon-home'></i></a>
|
||||
<a href='/' class='badge-category home' {{bindAttr style="badgeStyle"}}><i class='icon icon-home'></i></a>
|
||||
{{/if}}
|
||||
|
||||
{{#if categories}}
|
||||
<button {{action expand}}><i {{bindAttr class='iconClass'}}></i></button>
|
||||
|
||||
<section {{bindAttr class="expanded::hidden :category-menu"}} class='chooser'>
|
||||
<a href='#' {{action expand}} class='badge-category category-dropdown-button' {{bindAttr style="badgeStyle"}}><i {{bindAttr class="iconClass"}}></i></a>
|
||||
<section {{bindAttr class="expanded::hidden :category-dropdown-menu"}} class='chooser'>
|
||||
{{#each categories}}
|
||||
<div class='cat'>{{categoryLink this}}</div>
|
||||
{{/each}}
|
||||
|
|
|
@ -330,7 +330,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
|
||||
var category = this.get('controller.content.category');
|
||||
if (category) {
|
||||
opts.catLink = Discourse.Utilities.categoryLink(category);
|
||||
opts.catLink = Discourse.HTML.categoryLink(category);
|
||||
} else {
|
||||
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (I18n.t("topic.browse_all_categories")) + "</a>";
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Notification badge
|
||||
// --------------------------------------------------
|
||||
|
||||
|
|
|
@ -350,12 +350,28 @@
|
|||
// --------------------------------------------------
|
||||
|
||||
.list-controls {
|
||||
.home {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.badge-category {
|
||||
padding: 4px 10px;
|
||||
display: inline-block;
|
||||
background-color: yellow;
|
||||
margin: 8px 0 0 8px;
|
||||
line-height: 24px;
|
||||
float: left;
|
||||
}
|
||||
.category-dropdown-button {
|
||||
padding: 4px 10px 3px 8px;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.15);
|
||||
font-size: 16px;
|
||||
width: 10px;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
clear: both;
|
||||
}
|
||||
#list-area {
|
||||
|
@ -364,8 +380,6 @@
|
|||
|
||||
.topic-statuses .topic-status i {font-size: 15px;}
|
||||
|
||||
|
||||
|
||||
.empty-topic-list {
|
||||
padding: 10px;
|
||||
}
|
||||
|
@ -396,3 +410,27 @@ span.posted {
|
|||
image: image-url("posted.png");
|
||||
};
|
||||
}
|
||||
|
||||
.category-dropdown-menu {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
position: absolute;
|
||||
border: 1px solid #ccc;
|
||||
background-color: white;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
padding: 8px 5px 0 7px;
|
||||
z-index: 100;
|
||||
margin-top: 31px;
|
||||
|
||||
.badge-category {
|
||||
font-size: 13px;
|
||||
line-height: 26px;
|
||||
padding: 0px 8px;
|
||||
float: none;
|
||||
}
|
||||
|
||||
div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,97 +208,5 @@ ol.category-breadcrumb {
|
|||
li {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
border: 1px solid transparent;
|
||||
line-height: 31px;
|
||||
|
||||
&.no-category {
|
||||
font-size: 22px;
|
||||
|
||||
a {
|
||||
margin-right: 3px;
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding-left: 6px;
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-drop:hover {
|
||||
background-color: #f9f9f9;
|
||||
button {
|
||||
border-left: 1px solid #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
.category-menu {
|
||||
margin-left: -1px;
|
||||
padding: 8px 5px 0 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-category {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
padding: 0px 10px;
|
||||
float: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
padding: 0 4px 0 2px;
|
||||
margin: 0;
|
||||
height: 22px;
|
||||
color: #666;
|
||||
line-height: 13px;
|
||||
text-align: center;
|
||||
|
||||
i.icon-caret-down {
|
||||
margin-left: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-drop:hover {
|
||||
background-color: #eee;
|
||||
border: 1px solid #bbb;
|
||||
|
||||
button {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.category-menu {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
position: absolute;
|
||||
border: 1px solid #ccc;
|
||||
background-color: white;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
padding: 8px 5px 0 7px;
|
||||
z-index: 100;
|
||||
|
||||
.badge-category {
|
||||
font-size: 13px;
|
||||
line-height: 26px;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -242,10 +242,13 @@
|
|||
.list-controls {
|
||||
.badge-category {
|
||||
display: inline-block;
|
||||
background-color: yellow;
|
||||
margin: 8px 0 0 8px;
|
||||
float: left;
|
||||
}
|
||||
.category-dropdown {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
clear: both;
|
||||
}
|
||||
#list-area {
|
||||
|
@ -371,9 +374,6 @@ ol.category-breadcrumb {
|
|||
.icon-home {
|
||||
font-size: 20px;
|
||||
}
|
||||
.badge-category {
|
||||
margin: 3px 8px 0;
|
||||
}
|
||||
button {
|
||||
padding: 5px 10px;
|
||||
font-size: 16px;
|
||||
|
|
|
@ -210,8 +210,10 @@ Discourse::Application.routes.draw do
|
|||
get "#{filter}" => "list##{filter}"
|
||||
get "#{filter}/more" => "list##{filter}"
|
||||
|
||||
get "category/:category/#{filter}" => "list##{filter}"
|
||||
get "category/:category/#{filter}/more" => "list##{filter}"
|
||||
get "category/:category/l/#{filter}" => "list##{filter}"
|
||||
get "category/:category/l/#{filter}/more" => "list##{filter}"
|
||||
get "category/:parent_category/:category/l/#{filter}" => "list##{filter}"
|
||||
get "category/:parent_category/:category/l/#{filter}/more" => "list##{filter}"
|
||||
end
|
||||
|
||||
get 'category/:parent_category/:category' => 'list#category', as: 'category_list_parent'
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,12 +1,4 @@
|
|||
// Test helpers
|
||||
// var resolvingPromise = Ember.Deferred.promise(function (p) {
|
||||
// p.resolve();
|
||||
// });
|
||||
|
||||
// var resolvingPromiseWith = function(result) {
|
||||
// return Ember.Deferred.promise(function (p) { p.resolve(result); });
|
||||
// };
|
||||
|
||||
function exists(selector) {
|
||||
return !!count(selector);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
function parseHTML(rawHtml) {
|
||||
var builder = new Tautologistics.NodeHtmlParser.HtmlBuilder(),
|
||||
parser = new Tautologistics.NodeHtmlParser.Parser(builder);
|
||||
|
||||
parser.parseComplete(rawHtml);
|
||||
return builder.dom;
|
||||
}
|
|
@ -124,6 +124,7 @@ var jsHintOpts = {
|
|||
"controllerFor",
|
||||
"testController",
|
||||
"containsInstance",
|
||||
"parseHTML",
|
||||
"deepEqual",
|
||||
"notEqual",
|
||||
"Blob",
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
module("Discourse.HTML");
|
||||
|
||||
var html = Discourse.HTML;
|
||||
|
||||
test("categoryLink without a category", function() {
|
||||
blank(Discourse.HTML.categoryLink(), "it returns no HTML");
|
||||
});
|
||||
|
||||
test("Regular categoryLink", function() {
|
||||
var category = Discourse.Category.create({
|
||||
name: 'hello',
|
||||
id: 123,
|
||||
description: 'cool description',
|
||||
color: 'ff0',
|
||||
text_color: 'f00'
|
||||
}),
|
||||
tag = parseHTML(Discourse.HTML.categoryLink(category))[0];
|
||||
|
||||
equal(tag.name, 'a', 'it creates an `a` tag');
|
||||
equal(tag.attributes['class'], 'badge-category', 'it has the correct class');
|
||||
equal(tag.attributes.title, 'cool description', 'it has the correct title');
|
||||
|
||||
ok(tag.attributes.style.indexOf('#ff0') !== -1, "it has the color style");
|
||||
ok(tag.attributes.style.indexOf('#f00') !== -1, "it has the textColor style");
|
||||
|
||||
equal(tag.children[0].data, 'hello', 'it has the category name');
|
||||
});
|
||||
|
||||
test("undefined color", function() {
|
||||
var noColor = Discourse.Category.create({ name: 'hello', id: 123 }),
|
||||
tag = parseHTML(Discourse.HTML.categoryLink(noColor))[0];
|
||||
|
||||
blank(tag.attributes.style, "it has no color style because there are no colors");
|
||||
});
|
||||
|
||||
test("allowUncategorized", function() {
|
||||
var uncategorized = Discourse.Category.create({name: 'uncategorized', id: 345});
|
||||
this.stub(Discourse.Site, 'currentProp').withArgs('uncategorized_category_id').returns(345);
|
||||
|
||||
blank(Discourse.HTML.categoryLink(uncategorized), "it doesn't return HTML for uncategorized by default");
|
||||
present(Discourse.HTML.categoryLink(uncategorized, {allowUncategorized: true}), "it returns HTML");
|
||||
});
|
||||
|
|
@ -50,6 +50,7 @@
|
|||
//= require mousetrap.js
|
||||
//= require rsvp.js
|
||||
//= require show-html.js
|
||||
//= require htmlparser.js
|
||||
|
||||
// Stuff we need to load first
|
||||
//= require main_include
|
||||
|
|
|
@ -0,0 +1,993 @@
|
|||
/***********************************************
|
||||
Copyright 2010 - 2012 Chris Winberry <chris@winberry.net>. All rights reserved.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
***********************************************/
|
||||
/* v2.0.0 */
|
||||
|
||||
(function () {
|
||||
|
||||
var exports;
|
||||
if (typeof(module) !== 'undefined' && typeof(module.exports) !== 'undefined') {
|
||||
exports = module.exports;
|
||||
} else {
|
||||
exports = {};
|
||||
if (!this.Tautologistics) {
|
||||
this.Tautologistics = {};
|
||||
}
|
||||
if (this.Tautologistics.NodeHtmlParser) {
|
||||
return;
|
||||
}
|
||||
this.Tautologistics.NodeHtmlParser = exports;
|
||||
}
|
||||
|
||||
function inherits (ctor, superCtor) {
|
||||
var tempCtor = function(){};
|
||||
tempCtor.prototype = superCtor.prototype;
|
||||
ctor.super_ = superCtor;
|
||||
ctor.prototype = new tempCtor();
|
||||
ctor.prototype.constructor = ctor;
|
||||
}
|
||||
|
||||
var Mode = {
|
||||
Text: 'text',
|
||||
Tag: 'tag',
|
||||
Attr: 'attr',
|
||||
CData: 'cdata',
|
||||
Doctype: 'doctype',
|
||||
Comment: 'comment'
|
||||
};
|
||||
|
||||
function Parser (builder, options) {
|
||||
this._options = options ? options : { };
|
||||
// if (this._options.includeLocation === undefined) {
|
||||
// this._options.includeLocation = false; //Include position of element (row, col) on nodes
|
||||
// }
|
||||
this._validateBuilder(builder);
|
||||
var self = this;
|
||||
this._builder = builder;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
if (typeof(module) !== 'undefined' && typeof(module.exports) !== 'undefined') {
|
||||
|
||||
var Stream = require('stream');
|
||||
inherits(Parser, Stream);
|
||||
|
||||
Parser.prototype.writable = true;
|
||||
Parser.prototype.write = function(data) {
|
||||
if(data instanceof Buffer) {
|
||||
data = data.toString();
|
||||
}
|
||||
this.parseChunk(data);
|
||||
};
|
||||
|
||||
Parser.prototype.end = function(data) {
|
||||
if (arguments.length) {
|
||||
this.write(data);
|
||||
}
|
||||
this.writable = false;
|
||||
this.done();
|
||||
};
|
||||
|
||||
Parser.prototype.destroy = function() {
|
||||
this.writable = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
//**Public**//
|
||||
Parser.prototype.reset = function Parser$reset () {
|
||||
this._state = {
|
||||
mode: Mode.Text,
|
||||
pos: 0,
|
||||
data: null,
|
||||
pendingText: null,
|
||||
pendingWrite: null,
|
||||
lastTag: null,
|
||||
isScript: false,
|
||||
needData: false,
|
||||
output: [],
|
||||
done: false//,
|
||||
// line: 1,
|
||||
// col: 1
|
||||
};
|
||||
this._builder.reset();
|
||||
};
|
||||
|
||||
Parser.prototype.parseChunk = function Parser$parseChunk (chunk) {
|
||||
this._state.needData = false;
|
||||
this._state.data = (this._state.data !== null) ?
|
||||
this._state.data.substr(this.pos) + chunk
|
||||
:
|
||||
chunk
|
||||
;
|
||||
while (this._state.pos < this._state.data.length && !this._state.needData) {
|
||||
this._parse(this._state);
|
||||
}
|
||||
};
|
||||
|
||||
Parser.prototype.parseComplete = function Parser$parseComplete (data) {
|
||||
this.reset();
|
||||
this.parseChunk(data);
|
||||
this.done();
|
||||
};
|
||||
|
||||
Parser.prototype.done = function Parser$done () {
|
||||
this._state.done = true;
|
||||
this._parse(this._state);
|
||||
this._flushWrite();
|
||||
this._builder.done();
|
||||
};
|
||||
|
||||
//**Private**//
|
||||
Parser.prototype._validateBuilder = function Parser$_validateBuilder (builder) {
|
||||
if ((typeof builder) != "object") {
|
||||
throw new Error("Builder is not an object");
|
||||
}
|
||||
if ((typeof builder.reset) != "function") {
|
||||
throw new Error("Builder method 'reset' is invalid");
|
||||
}
|
||||
if ((typeof builder.done) != "function") {
|
||||
throw new Error("Builder method 'done' is invalid");
|
||||
}
|
||||
if ((typeof builder.write) != "function") {
|
||||
throw new Error("Builder method 'write' is invalid");
|
||||
}
|
||||
if ((typeof builder.error) != "function") {
|
||||
throw new Error("Builder method 'error' is invalid");
|
||||
}
|
||||
};
|
||||
|
||||
Parser.prototype._parse = function Parser$_parse () {
|
||||
switch (this._state.mode) {
|
||||
case Mode.Text:
|
||||
return this._parseText(this._state);
|
||||
case Mode.Tag:
|
||||
return this._parseTag(this._state);
|
||||
case Mode.Attr:
|
||||
return this._parseAttr(this._state);
|
||||
case Mode.CData:
|
||||
return this._parseCData(this._state);
|
||||
case Mode.Doctype:
|
||||
return this._parseDoctype(this._state);
|
||||
case Mode.Comment:
|
||||
return this._parseComment(this._state);
|
||||
}
|
||||
};
|
||||
|
||||
Parser.prototype._writePending = function Parser$_writePending (node) {
|
||||
if (!this._state.pendingWrite) {
|
||||
this._state.pendingWrite = [];
|
||||
}
|
||||
this._state.pendingWrite.push(node);
|
||||
};
|
||||
|
||||
Parser.prototype._flushWrite = function Parser$_flushWrite () {
|
||||
if (this._state.pendingWrite) {
|
||||
for (var i = 0, len = this._state.pendingWrite.length; i < len; i++) {
|
||||
var node = this._state.pendingWrite[i];
|
||||
this._builder.write(node);
|
||||
}
|
||||
this._state.pendingWrite = null;
|
||||
}
|
||||
};
|
||||
|
||||
Parser.prototype._write = function Parser$_write (node) {
|
||||
this._flushWrite();
|
||||
this._builder.write(node);
|
||||
};
|
||||
|
||||
Parser._re_parseText_scriptClose = /<\s*\/\s*script/ig;
|
||||
Parser.prototype._parseText = function Parser$_parseText () {
|
||||
var state = this._state;
|
||||
var foundPos;
|
||||
if (state.isScript) {
|
||||
Parser._re_parseText_scriptClose.lastIndex = state.pos;
|
||||
foundPos = Parser._re_parseText_scriptClose.exec(state.data);
|
||||
foundPos = (foundPos) ?
|
||||
foundPos.index
|
||||
:
|
||||
-1
|
||||
;
|
||||
} else {
|
||||
foundPos = state.data.indexOf('<', state.pos);
|
||||
}
|
||||
var text = (foundPos === -1) ? state.data.substring(state.pos, state.data.length) : state.data.substring(state.pos, foundPos);
|
||||
if (foundPos < 0 && state.done) {
|
||||
foundPos = state.data.length;
|
||||
}
|
||||
if (foundPos < 0) {
|
||||
if (state.isScript) {
|
||||
state.needData = true;
|
||||
return;
|
||||
}
|
||||
if (!state.pendingText) {
|
||||
state.pendingText = [];
|
||||
}
|
||||
state.pendingText.push(state.data.substring(state.pos, state.data.length));
|
||||
state.pos = state.data.length;
|
||||
} else {
|
||||
if (state.pendingText) {
|
||||
state.pendingText.push(state.data.substring(state.pos, foundPos));
|
||||
text = state.pendingText.join('');
|
||||
state.pendingText = null;
|
||||
} else {
|
||||
text = state.data.substring(state.pos, foundPos);
|
||||
}
|
||||
if (text !== '') {
|
||||
this._write({ type: Mode.Text, data: text });
|
||||
}
|
||||
state.pos = foundPos + 1;
|
||||
state.mode = Mode.Tag;
|
||||
}
|
||||
};
|
||||
|
||||
Parser.re_parseTag = /\s*(\/?)\s*([^\s>\/]+)(\s*)\??(>?)/g;
|
||||
Parser.prototype._parseTag = function Parser$_parseTag () {
|
||||
var state = this._state;
|
||||
Parser.re_parseTag.lastIndex = state.pos;
|
||||
var match = Parser.re_parseTag.exec(state.data);
|
||||
if (match) {
|
||||
if (!match[1] && match[2].substr(0, 3) === '!--') {
|
||||
state.mode = Mode.Comment;
|
||||
state.pos += 3;
|
||||
return;
|
||||
}
|
||||
if (!match[1] && match[2].substr(0, 8) === '![CDATA[') {
|
||||
state.mode = Mode.CData;
|
||||
state.pos += 8;
|
||||
return;
|
||||
}
|
||||
if (!match[1] && match[2].substr(0, 8) === '!DOCTYPE') {
|
||||
state.mode = Mode.Doctype;
|
||||
state.pos += 8;
|
||||
return;
|
||||
}
|
||||
if (!state.done && (state.pos + match[0].length) === state.data.length) {
|
||||
//We're at the and of the data, might be incomplete
|
||||
state.needData = true;
|
||||
return;
|
||||
}
|
||||
var raw;
|
||||
if (match[4] === '>') {
|
||||
state.mode = Mode.Text;
|
||||
raw = match[0].substr(0, match[0].length - 1);
|
||||
} else {
|
||||
state.mode = Mode.Attr;
|
||||
raw = match[0];
|
||||
}
|
||||
state.pos += match[0].length;
|
||||
var tag = { type: Mode.Tag, name: match[1] + match[2], raw: raw };
|
||||
if (state.mode === Mode.Attr) {
|
||||
state.lastTag = tag;
|
||||
}
|
||||
if (tag.name.toLowerCase() === 'script') {
|
||||
state.isScript = true;
|
||||
} else if (tag.name.toLowerCase() === '/script') {
|
||||
state.isScript = false;
|
||||
}
|
||||
if (state.mode === Mode.Attr) {
|
||||
this._writePending(tag);
|
||||
} else {
|
||||
this._write(tag);
|
||||
}
|
||||
} else {
|
||||
//TODO: end of tag?
|
||||
//TODO: push to pending?
|
||||
state.needData = true;
|
||||
}
|
||||
};
|
||||
|
||||
Parser.re_parseAttr_findName = /\s*([^=<>\s'"\/]+)\s*/g;
|
||||
Parser.prototype._parseAttr_findName = function Parser$_parseAttr_findName () {
|
||||
Parser.re_parseAttr_findName.lastIndex = this._state.pos;
|
||||
var match = Parser.re_parseAttr_findName.exec(this._state.data);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
if (this._state.pos + match[0].length !== Parser.re_parseAttr_findName.lastIndex) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
match: match[0]
|
||||
, name: match[1]
|
||||
};
|
||||
};
|
||||
Parser.re_parseAttr_findValue = /\s*=\s*(?:'([^']*)'|"([^"]*)"|([^'"\s\/>]+))\s*/g;
|
||||
Parser.re_parseAttr_findValue_last = /\s*=\s*['"]?(.*)$/g;
|
||||
Parser.prototype._parseAttr_findValue = function Parser$_parseAttr_findValue () {
|
||||
var state = this._state;
|
||||
Parser.re_parseAttr_findValue.lastIndex = state.pos;
|
||||
var match = Parser.re_parseAttr_findValue.exec(state.data);
|
||||
if (!match) {
|
||||
if (!state.done) {
|
||||
return null;
|
||||
}
|
||||
Parser.re_parseAttr_findValue_last.lastIndex = state.pos;
|
||||
match = Parser.re_parseAttr_findValue_last.exec(state.data);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
match: match[0]
|
||||
, value: (match[1] !== '') ? match[1] : null
|
||||
};
|
||||
}
|
||||
if (state.pos + match[0].length !== Parser.re_parseAttr_findValue.lastIndex) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
match: match[0]
|
||||
, value: match[1] || match[2] || match[3]
|
||||
};
|
||||
};
|
||||
Parser.re_parseAttr_splitValue = /\s*=\s*['"]?/g;
|
||||
Parser.re_parseAttr_selfClose = /(\s*\/\s*)(>?)/g;
|
||||
Parser.prototype._parseAttr = function Parser$_parseAttr () {
|
||||
var state = this._state;
|
||||
var name_data = this._parseAttr_findName(state);
|
||||
if (!name_data || name_data.name === '?') {
|
||||
Parser.re_parseAttr_selfClose.lastIndex = state.pos;
|
||||
var matchTrailingSlash = Parser.re_parseAttr_selfClose.exec(state.data);
|
||||
if (matchTrailingSlash && matchTrailingSlash.index === state.pos) {
|
||||
if (!state.done && !matchTrailingSlash[2] && state.pos + matchTrailingSlash[0].length === state.data.length) {
|
||||
state.needData = true;
|
||||
return;
|
||||
}
|
||||
state.lastTag.raw += matchTrailingSlash[1];
|
||||
// state.output.push({ type: Mode.Tag, name: '/' + state.lastTag.name, raw: null });
|
||||
this._write({ type: Mode.Tag, name: '/' + state.lastTag.name, raw: null });
|
||||
state.pos += matchTrailingSlash[1].length;
|
||||
}
|
||||
var foundPos = state.data.indexOf('>', state.pos);
|
||||
if (foundPos < 0) {
|
||||
if (state.done) { //TODO: is this needed?
|
||||
state.lastTag.raw += state.data.substr(state.pos);
|
||||
state.pos = state.data.length;
|
||||
return;
|
||||
}
|
||||
state.needData = true;
|
||||
} else {
|
||||
// state.lastTag = null;
|
||||
state.pos = foundPos + 1;
|
||||
state.mode = Mode.Text;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!state.done && state.pos + name_data.match.length === state.data.length) {
|
||||
state.needData = true;
|
||||
return null;
|
||||
}
|
||||
state.pos += name_data.match.length;
|
||||
var value_data = this._parseAttr_findValue(state);
|
||||
if (value_data) {
|
||||
if (!state.done && state.pos + value_data.match.length === state.data.length) {
|
||||
state.needData = true;
|
||||
state.pos -= name_data.match.length;
|
||||
return;
|
||||
}
|
||||
state.pos += value_data.match.length;
|
||||
} else {
|
||||
Parser.re_parseAttr_splitValue.lastIndex = state.pos;
|
||||
if (Parser.re_parseAttr_splitValue.exec(state.data)) {
|
||||
state.needData = true;
|
||||
state.pos -= name_data.match.length;
|
||||
return;
|
||||
}
|
||||
value_data = {
|
||||
match: ''
|
||||
, value: null
|
||||
};
|
||||
}
|
||||
state.lastTag.raw += name_data.match + value_data.match;
|
||||
|
||||
this._writePending({ type: Mode.Attr, name: name_data.name, data: value_data.value });
|
||||
};
|
||||
|
||||
Parser.re_parseCData_findEnding = /\]{1,2}$/;
|
||||
Parser.prototype._parseCData = function Parser$_parseCData () {
|
||||
var state = this._state;
|
||||
var foundPos = state.data.indexOf(']]>', state.pos);
|
||||
if (foundPos < 0 && state.done) {
|
||||
foundPos = state.data.length;
|
||||
}
|
||||
if (foundPos < 0) {
|
||||
Parser.re_parseCData_findEnding.lastIndex = state.pos;
|
||||
var matchPartialCDataEnd = Parser.re_parseCData_findEnding.exec(state.data);
|
||||
if (matchPartialCDataEnd) {
|
||||
state.needData = true;
|
||||
return;
|
||||
}
|
||||
if (!state.pendingText) {
|
||||
state.pendingText = [];
|
||||
}
|
||||
state.pendingText.push(state.data.substr(state.pos, state.data.length));
|
||||
state.pos = state.data.length;
|
||||
state.needData = true;
|
||||
} else {
|
||||
var text;
|
||||
if (state.pendingText) {
|
||||
state.pendingText.push(state.data.substring(state.pos, foundPos));
|
||||
text = state.pendingText.join('');
|
||||
state.pendingText = null;
|
||||
} else {
|
||||
text = state.data.substring(state.pos, foundPos);
|
||||
}
|
||||
this._write({ type: Mode.CData, data: text });
|
||||
state.mode = Mode.Text;
|
||||
state.pos = foundPos + 3;
|
||||
}
|
||||
};
|
||||
|
||||
Parser.prototype._parseDoctype = function Parser$_parseDoctype () {
|
||||
var state = this._state;
|
||||
var foundPos = state.data.indexOf('>', state.pos);
|
||||
if (foundPos < 0 && state.done) {
|
||||
foundPos = state.data.length;
|
||||
}
|
||||
if (foundPos < 0) {
|
||||
Parser.re_parseCData_findEnding.lastIndex = state.pos;
|
||||
if (!state.pendingText) {
|
||||
state.pendingText = [];
|
||||
}
|
||||
state.pendingText.push(state.data.substr(state.pos, state.data.length));
|
||||
state.pos = state.data.length;
|
||||
state.needData = true;
|
||||
} else {
|
||||
var text;
|
||||
if (state.pendingText) {
|
||||
state.pendingText.push(state.data.substring(state.pos, foundPos));
|
||||
text = state.pendingText.join('');
|
||||
state.pendingText = null;
|
||||
} else {
|
||||
text = state.data.substring(state.pos, foundPos);
|
||||
}
|
||||
this._write({ type: Mode.Doctype, data: text });
|
||||
state.mode = Mode.Text;
|
||||
state.pos = foundPos + 1;
|
||||
}
|
||||
};
|
||||
|
||||
Parser.re_parseComment_findEnding = /\-{1,2}$/;
|
||||
Parser.prototype._parseComment = function Parser$_parseComment () {
|
||||
var state = this._state;
|
||||
var foundPos = state.data.indexOf('-->', state.pos);
|
||||
if (foundPos < 0 && state.done) {
|
||||
foundPos = state.data.length;
|
||||
}
|
||||
if (foundPos < 0) {
|
||||
Parser.re_parseComment_findEnding.lastIndex = state.pos;
|
||||
var matchPartialCommentEnd = Parser.re_parseComment_findEnding.exec(state.data);
|
||||
if (matchPartialCommentEnd) {
|
||||
state.needData = true;
|
||||
return;
|
||||
}
|
||||
if (!state.pendingText) {
|
||||
state.pendingText = [];
|
||||
}
|
||||
state.pendingText.push(state.data.substr(state.pos, state.data.length));
|
||||
state.pos = state.data.length;
|
||||
state.needData = true;
|
||||
} else {
|
||||
var text;
|
||||
if (state.pendingText) {
|
||||
state.pendingText.push(state.data.substring(state.pos, foundPos));
|
||||
text = state.pendingText.join('');
|
||||
state.pendingText = null;
|
||||
} else {
|
||||
text = state.data.substring(state.pos, foundPos);
|
||||
}
|
||||
// state.output.push({ type: Mode.Comment, data: text });
|
||||
this._write({ type: Mode.Comment, data: text });
|
||||
state.mode = Mode.Text;
|
||||
state.pos = foundPos + 3;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function HtmlBuilder (callback, options) {
|
||||
this.reset();
|
||||
this._options = options ? options : { };
|
||||
if (this._options.ignoreWhitespace === undefined) {
|
||||
this._options.ignoreWhitespace = false; //Keep whitespace-only text nodes
|
||||
}
|
||||
if (this._options.includeLocation === undefined) {
|
||||
this._options.includeLocation = false; //Include position of element (row, col) on nodes
|
||||
}
|
||||
if (this._options.verbose === undefined) {
|
||||
this._options.verbose = true; //Keep data property for tags and raw property for all
|
||||
}
|
||||
if (this._options.enforceEmptyTags === undefined) {
|
||||
this._options.enforceEmptyTags = true; //Don't allow children for HTML tags defined as empty in spec
|
||||
}
|
||||
if (this._options.caseSensitiveTags === undefined) {
|
||||
this._options.caseSensitiveTags = false; //Lowercase all tag names
|
||||
}
|
||||
if (this._options.caseSensitiveAttr === undefined) {
|
||||
this._options.caseSensitiveAttr = false; //Lowercase all attribute names
|
||||
}
|
||||
if ((typeof callback) == "function") {
|
||||
this._callback = callback;
|
||||
}
|
||||
}
|
||||
|
||||
//**"Static"**//
|
||||
//HTML Tags that shouldn't contain child nodes
|
||||
HtmlBuilder._emptyTags = {
|
||||
area: 1
|
||||
, base: 1
|
||||
, basefont: 1
|
||||
, br: 1
|
||||
, col: 1
|
||||
, frame: 1
|
||||
, hr: 1
|
||||
, img: 1
|
||||
, input: 1
|
||||
, isindex: 1
|
||||
, link: 1
|
||||
, meta: 1
|
||||
, param: 1
|
||||
, embed: 1
|
||||
, '?xml': 1
|
||||
};
|
||||
//Regex to detect whitespace only text nodes
|
||||
HtmlBuilder.reWhitespace = /^\s*$/;
|
||||
|
||||
//**Public**//
|
||||
//Properties//
|
||||
HtmlBuilder.prototype.dom = null; //The hierarchical object containing the parsed HTML
|
||||
//Methods//
|
||||
//Resets the builder back to starting state
|
||||
HtmlBuilder.prototype.reset = function HtmlBuilder$reset() {
|
||||
this.dom = [];
|
||||
// this._raw = [];
|
||||
this._done = false;
|
||||
this._tagStack = [];
|
||||
this._lastTag = null;
|
||||
this._tagStack.last = function HtmlBuilder$_tagStack$last () {
|
||||
return(this.length ? this[this.length - 1] : null);
|
||||
};
|
||||
this._line = 1;
|
||||
this._col = 1;
|
||||
};
|
||||
//Signals the builder that parsing is done
|
||||
HtmlBuilder.prototype.done = function HtmlBuilder$done () {
|
||||
this._done = true;
|
||||
this.handleCallback(null);
|
||||
};
|
||||
|
||||
HtmlBuilder.prototype.error = function HtmlBuilder$error (error) {
|
||||
this.handleCallback(error);
|
||||
};
|
||||
|
||||
HtmlBuilder.prototype.handleCallback = function HtmlBuilder$handleCallback (error) {
|
||||
if ((typeof this._callback) != "function") {
|
||||
if (error) {
|
||||
throw error;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._callback(error, this.dom);
|
||||
};
|
||||
|
||||
HtmlBuilder.prototype.isEmptyTag = function HtmlBuilder$isEmptyTag (element) {
|
||||
var name = element.name.toLowerCase();
|
||||
if (name.charAt(0) == '?') {
|
||||
return true;
|
||||
}
|
||||
if (name.charAt(0) == '/') {
|
||||
name = name.substring(1);
|
||||
}
|
||||
return this._options.enforceEmptyTags && !!HtmlBuilder._emptyTags[name];
|
||||
};
|
||||
|
||||
HtmlBuilder.prototype._getLocation = function HtmlBuilder$_getLocation () {
|
||||
return { line: this._line, col: this._col };
|
||||
};
|
||||
|
||||
// HtmlBuilder.reLineSplit = /(\r\n|\r|\n)/g;
|
||||
HtmlBuilder.prototype._updateLocation = function HtmlBuilder$_updateLocation (node) {
|
||||
var positionData = (node.type === Mode.Tag) ? node.raw : node.data;
|
||||
if (positionData === null) {
|
||||
return;
|
||||
}
|
||||
// var lines = positionData.split(HtmlBuilder.reLineSplit);
|
||||
var lines = positionData.split("\n");
|
||||
this._line += lines.length - 1;
|
||||
if (lines.length > 1) {
|
||||
this._col = 1;
|
||||
}
|
||||
this._col += lines[lines.length - 1].length;
|
||||
if (node.type === Mode.Tag) {
|
||||
this._col += 2;
|
||||
} else if (node.type === Mode.Comment) {
|
||||
this._col += 7;
|
||||
} else if (node.type === Mode.CData) {
|
||||
this._col += 12;
|
||||
}
|
||||
};
|
||||
|
||||
HtmlBuilder.prototype._copyElement = function HtmlBuilder$_copyElement (element) {
|
||||
var newElement = { type: element.type };
|
||||
|
||||
if (this._options.verbose && element['raw'] !== undefined) {
|
||||
newElement.raw = element.raw;
|
||||
}
|
||||
if (element['name'] !== undefined) {
|
||||
switch (element.type) {
|
||||
|
||||
case Mode.Tag:
|
||||
newElement.name = this._options.caseSensitiveTags ?
|
||||
element.name
|
||||
:
|
||||
element.name.toLowerCase()
|
||||
;
|
||||
break;
|
||||
|
||||
case Mode.Attr:
|
||||
newElement.name = this._options.caseSensitiveAttr ?
|
||||
element.name
|
||||
:
|
||||
element.name.toLowerCase()
|
||||
;
|
||||
break;
|
||||
|
||||
default:
|
||||
newElement.name = this._options.caseSensitiveTags ?
|
||||
element.name
|
||||
:
|
||||
element.name.toLowerCase()
|
||||
;
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
if (element['data'] !== undefined) {
|
||||
newElement.data = element.data;
|
||||
}
|
||||
if (element.location) {
|
||||
newElement.location = { line: element.location.line, col: element.location.col };
|
||||
}
|
||||
|
||||
return newElement;
|
||||
};
|
||||
|
||||
HtmlBuilder.prototype.write = function HtmlBuilder$write (element) {
|
||||
// this._raw.push(element);
|
||||
if (this._done) {
|
||||
this.handleCallback(new Error("Writing to the builder after done() called is not allowed without a reset()"));
|
||||
}
|
||||
if (this._options.includeLocation) {
|
||||
if (element.type !== Mode.Attr) {
|
||||
element.location = this._getLocation();
|
||||
this._updateLocation(element);
|
||||
}
|
||||
}
|
||||
if (element.type === Mode.Text && this._options.ignoreWhitespace) {
|
||||
if (HtmlBuilder.reWhitespace.test(element.data)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
var parent;
|
||||
var node;
|
||||
if (!this._tagStack.last()) { //There are no parent elements
|
||||
//If the element can be a container, add it to the tag stack and the top level list
|
||||
if (element.type === Mode.Tag) {
|
||||
if (element.name.charAt(0) != "/") { //Ignore closing tags that obviously don't have an opening tag
|
||||
node = this._copyElement(element);
|
||||
this.dom.push(node);
|
||||
if (!this.isEmptyTag(node)) { //Don't add tags to the tag stack that can't have children
|
||||
this._tagStack.push(node);
|
||||
}
|
||||
this._lastTag = node;
|
||||
}
|
||||
} else if (element.type === Mode.Attr && this._lastTag) {
|
||||
if (!this._lastTag.attributes) {
|
||||
this._lastTag.attributes = {};
|
||||
}
|
||||
this._lastTag.attributes[this._options.caseSensitiveAttr ? element.name : element.name.toLowerCase()] =
|
||||
element.data;
|
||||
} else { //Otherwise just add to the top level list
|
||||
this.dom.push(this._copyElement(element));
|
||||
}
|
||||
} else { //There are parent elements
|
||||
//If the element can be a container, add it as a child of the element
|
||||
//on top of the tag stack and then add it to the tag stack
|
||||
if (element.type === Mode.Tag) {
|
||||
if (element.name.charAt(0) == "/") {
|
||||
//This is a closing tag, scan the tagStack to find the matching opening tag
|
||||
//and pop the stack up to the opening tag's parent
|
||||
var baseName = this._options.caseSensitiveTags ?
|
||||
element.name.substring(1)
|
||||
:
|
||||
element.name.substring(1).toLowerCase()
|
||||
;
|
||||
if (!this.isEmptyTag(element)) {
|
||||
var pos = this._tagStack.length - 1;
|
||||
while (pos > -1 && this._tagStack[pos--].name != baseName) { }
|
||||
if (pos > -1 || this._tagStack[0].name == baseName) {
|
||||
while (pos < this._tagStack.length - 1) {
|
||||
this._tagStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else { //This is not a closing tag
|
||||
parent = this._tagStack.last();
|
||||
if (element.type === Mode.Attr) {
|
||||
if (!parent.attributes) {
|
||||
parent.attributes = {};
|
||||
}
|
||||
parent.attributes[this._options.caseSensitiveAttr ? element.name : element.name.toLowerCase()] =
|
||||
element.data;
|
||||
} else {
|
||||
node = this._copyElement(element);
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
parent.children.push(node);
|
||||
if (!this.isEmptyTag(node)) { //Don't add tags to the tag stack that can't have children
|
||||
this._tagStack.push(node);
|
||||
}
|
||||
if (element.type === Mode.Tag) {
|
||||
this._lastTag = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else { //This is not a container element
|
||||
parent = this._tagStack.last();
|
||||
if (element.type === Mode.Attr) {
|
||||
if (!parent.attributes) {
|
||||
parent.attributes = {};
|
||||
}
|
||||
parent.attributes[this._options.caseSensitiveAttr ? element.name : element.name.toLowerCase()] =
|
||||
element.data;
|
||||
} else {
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
parent.children.push(this._copyElement(element));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//**Private**//
|
||||
//Properties//
|
||||
HtmlBuilder.prototype._options = null; //Builder options for how to behave
|
||||
HtmlBuilder.prototype._callback = null; //Callback to respond to when parsing done
|
||||
HtmlBuilder.prototype._done = false; //Flag indicating whether builder has been notified of parsing completed
|
||||
HtmlBuilder.prototype._tagStack = null; //List of parents to the currently element being processed
|
||||
//Methods//
|
||||
|
||||
|
||||
function RssBuilder (callback) {
|
||||
RssBuilder.super_.call(this, callback, { ignoreWhitespace: true, verbose: false, enforceEmptyTags: false, caseSensitiveTags: true });
|
||||
}
|
||||
inherits(RssBuilder, HtmlBuilder);
|
||||
|
||||
RssBuilder.prototype.done = function RssBuilder$done () {
|
||||
var feed = {};
|
||||
var feedRoot;
|
||||
|
||||
var found = DomUtils.getElementsByTagName(function (value) { return(value == "rss" || value == "feed"); }, this.dom, false);
|
||||
if (found.length) {
|
||||
feedRoot = found[0];
|
||||
}
|
||||
if (feedRoot) {
|
||||
if (feedRoot.name == "rss") {
|
||||
feed.type = "rss";
|
||||
feedRoot = feedRoot.children[0]; //<channel/>
|
||||
feed.id = "";
|
||||
try {
|
||||
feed.title = DomUtils.getElementsByTagName("title", feedRoot.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
feed.link = DomUtils.getElementsByTagName("link", feedRoot.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
feed.description = DomUtils.getElementsByTagName("description", feedRoot.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
feed.updated = new Date(DomUtils.getElementsByTagName("lastBuildDate", feedRoot.children, false)[0].children[0].data);
|
||||
} catch (ex) { }
|
||||
try {
|
||||
feed.author = DomUtils.getElementsByTagName("managingEditor", feedRoot.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
feed.items = [];
|
||||
DomUtils.getElementsByTagName("item", feedRoot.children).forEach(function (item, index, list) {
|
||||
var entry = {};
|
||||
try {
|
||||
entry.id = DomUtils.getElementsByTagName("guid", item.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
entry.title = DomUtils.getElementsByTagName("title", item.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
entry.link = DomUtils.getElementsByTagName("link", item.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
entry.description = DomUtils.getElementsByTagName("description", item.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
entry.pubDate = new Date(DomUtils.getElementsByTagName("pubDate", item.children, false)[0].children[0].data);
|
||||
} catch (ex) { }
|
||||
feed.items.push(entry);
|
||||
});
|
||||
} else {
|
||||
feed.type = "atom";
|
||||
try {
|
||||
feed.id = DomUtils.getElementsByTagName("id", feedRoot.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
feed.title = DomUtils.getElementsByTagName("title", feedRoot.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
feed.link = DomUtils.getElementsByTagName("link", feedRoot.children, false)[0].attributes.href;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
feed.description = DomUtils.getElementsByTagName("subtitle", feedRoot.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
feed.updated = new Date(DomUtils.getElementsByTagName("updated", feedRoot.children, false)[0].children[0].data);
|
||||
} catch (ex) { }
|
||||
try {
|
||||
feed.author = DomUtils.getElementsByTagName("email", feedRoot.children, true)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
feed.items = [];
|
||||
DomUtils.getElementsByTagName("entry", feedRoot.children).forEach(function (item, index, list) {
|
||||
var entry = {};
|
||||
try {
|
||||
entry.id = DomUtils.getElementsByTagName("id", item.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
entry.title = DomUtils.getElementsByTagName("title", item.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
entry.link = DomUtils.getElementsByTagName("link", item.children, false)[0].attributes.href;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
entry.description = DomUtils.getElementsByTagName("summary", item.children, false)[0].children[0].data;
|
||||
} catch (ex) { }
|
||||
try {
|
||||
entry.pubDate = new Date(DomUtils.getElementsByTagName("updated", item.children, false)[0].children[0].data);
|
||||
} catch (ex) { }
|
||||
feed.items.push(entry);
|
||||
});
|
||||
}
|
||||
|
||||
this.dom = feed;
|
||||
}
|
||||
RssBuilder.super_.prototype.done.call(this);
|
||||
};
|
||||
|
||||
var DomUtils = {
|
||||
testElement: function DomUtils$testElement (options, element) {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var key in options) {
|
||||
if (!options.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
if (key == "tag_name") {
|
||||
if (element.type !== Mode.Tag) {
|
||||
return false;
|
||||
}
|
||||
if (!options["tag_name"](element.name)) {
|
||||
return false;
|
||||
}
|
||||
} else if (key == "tag_type") {
|
||||
if (!options["tag_type"](element.type)) {
|
||||
return false;
|
||||
}
|
||||
} else if (key == "tag_contains") {
|
||||
if (element.type !== Mode.Text && element.type !== Mode.Comment && element.type !== Mode.CData) {
|
||||
return false;
|
||||
}
|
||||
if (!options["tag_contains"](element.data)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!element.attributes || !options[key](element.attributes[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
, getElements: function DomUtils$getElements (options, currentElement, recurse, limit) {
|
||||
recurse = (recurse === undefined || recurse === null) || !!recurse;
|
||||
limit = isNaN(parseInt(limit)) ? -1 : parseInt(limit);
|
||||
|
||||
if (!currentElement) {
|
||||
return([]);
|
||||
}
|
||||
|
||||
var found = [];
|
||||
var elementList;
|
||||
|
||||
function getTest (checkVal) {
|
||||
return function (value) {
|
||||
return(value == checkVal);
|
||||
};
|
||||
}
|
||||
for (var key in options) {
|
||||
if ((typeof options[key]) != "function") {
|
||||
options[key] = getTest(options[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (DomUtils.testElement(options, currentElement)) {
|
||||
found.push(currentElement);
|
||||
}
|
||||
|
||||
if (limit >= 0 && found.length >= limit) {
|
||||
return(found);
|
||||
}
|
||||
|
||||
if (recurse && currentElement.children) {
|
||||
elementList = currentElement.children;
|
||||
} else if (currentElement instanceof Array) {
|
||||
elementList = currentElement;
|
||||
} else {
|
||||
return(found);
|
||||
}
|
||||
|
||||
for (var i = 0; i < elementList.length; i++) {
|
||||
found = found.concat(DomUtils.getElements(options, elementList[i], recurse, limit));
|
||||
if (limit >= 0 && found.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return(found);
|
||||
}
|
||||
|
||||
, getElementById: function DomUtils$getElementById (id, currentElement, recurse) {
|
||||
var result = DomUtils.getElements({ id: id }, currentElement, recurse, 1);
|
||||
return(result.length ? result[0] : null);
|
||||
}
|
||||
|
||||
, getElementsByTagName: function DomUtils$getElementsByTagName (name, currentElement, recurse, limit) {
|
||||
return(DomUtils.getElements({ tag_name: name }, currentElement, recurse, limit));
|
||||
}
|
||||
|
||||
, getElementsByTagType: function DomUtils$getElementsByTagType (type, currentElement, recurse, limit) {
|
||||
return(DomUtils.getElements({ tag_type: type }, currentElement, recurse, limit));
|
||||
}
|
||||
};
|
||||
|
||||
exports.Parser = Parser;
|
||||
|
||||
exports.HtmlBuilder = HtmlBuilder;
|
||||
|
||||
exports.RssBuilder = RssBuilder;
|
||||
|
||||
exports.ElementType = Mode;
|
||||
|
||||
exports.DomUtils = DomUtils;
|
||||
|
||||
})();
|
Loading…
Reference in New Issue